MasatoMakino/pixijs-basic-scrollbar

View on GitHub
src/SliderView.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import {
  Container,
  FederatedPointerEvent,
  Graphics,
  Point,
  EventEmitter,
} from "pixi.js";
import {
  SliderEventContext,
  SliderEventTypes,
  SliderViewOption,
  SliderViewOptionUtil,
  SliderViewUtil,
} from "./index.js";
import { FederatedWheelEvent } from "pixi.js";

/**
 * スライダー用クラスです
 *
 * 使用上の注意 :
 * オブジェクトのサイズの計測にgetLocalBounds関数を使用しています。
 * hitAreaでサイズをあらかじめ与えてください。
 */

export class SliderView extends Container {
  protected readonly _base: Container; // スライダーの地
  protected readonly _bar?: Container; // スライドにあわせて表示されるバー

  protected readonly _barMask?: Graphics; // バーのマスク
  protected readonly _slideButton: Container; // スライドボタン
  readonly buttonRootContainer: Container | HTMLCanvasElement;

  protected _minPosition: number; // スライダーボタンの座標の最小値
  protected _maxPosition: number; // スライダーボタンの座標の最大値
  readonly isHorizontal: boolean = true;
  wheelSpeed: number = 0.0003;

  readonly canvas?: HTMLCanvasElement;

  protected readonly dragStartPos: Point = new Point();
  /**
   * 現在のスライダーの位置の割合。
   * MIN 0.0 ~ SliderView.MAX_RATE。
   */
  private _rate: number;
  get rate() {
    return this._rate;
  }
  public static readonly MAX_RATE: number = 1.0;
  private isDragging: Boolean = false; // 現在スライド中か否か
  readonly sliderEventEmitter = new EventEmitter<SliderEventTypes>();

  /**
   * @param option
   */
  constructor(option: SliderViewOption) {
    super();

    const initOption = SliderViewOptionUtil.init(option);

    this.canvas = initOption.canvas;
    this._base = this.initBase(initOption.base);
    this._bar = this.initBarAndMask(initOption?.bar);
    this._barMask = this.initBarAndMask(initOption?.mask) as Graphics;
    if (this._bar && this._barMask) this._bar.mask = this._barMask;

    this._slideButton = this.initSliderButton(initOption.button);
    this.buttonRootContainer = SliderViewUtil.getRootContainer(
      this.canvas,
      this._slideButton,
    );

    this._minPosition = initOption.minPosition;
    this._maxPosition = initOption.maxPosition;
    this.isHorizontal = initOption.isHorizontal;
    this._rate = initOption.rate;

    this.changeRate(this._rate);
  }

  /**
   * スライダーの位置を変更する
   * @param    rate    スライダーの位置 MIN 0.0 ~ MAX [SliderView.MAX_RATE]
   */
  public changeRate(rate: number): void {
    //ドラッグ中は外部からの操作を無視する。
    if (this.isDragging) return;

    this._rate = rate;
    const pos: number = this.convertRateToPixel(this._rate);
    this.updateParts(pos);

    this.sliderEventEmitter.emit(
      "slider_change",
      new SliderEventContext(this.rate),
    );
  }

  /**
   * スライダーのドラッグを開始する
   * @param {Object} e
   */
  private startMove = (e: FederatedPointerEvent) => {
    this.onPressedSliderButton(e as FederatedPointerEvent);
  };

  protected onPressedSliderButton(e: FederatedPointerEvent): void {
    this.isDragging = true;
    const target: Container = e.currentTarget as Container;

    const localPos = this.toLocal(e.global);
    this.dragStartPos.set(localPos.x - target.x, localPos.y - target.y);

    SliderViewUtil.addEventListenerToTarget(
      this.buttonRootContainer,
      "pointermove",
      this.moveSlider,
    );

    this._slideButton.on("pointerup", this.moveSliderFinish);
    this._slideButton.on("pointerupoutside", this.moveSliderFinish);
  }

  /**
   * スライダーのドラッグ中の処理
   * @param e
   */
  private moveSlider = (e: Event): void => {
    this.onMoveSlider(e as PointerEvent);
  };

  protected onMoveSlider(e: PointerEvent): void {
    const mousePos: number = this.limitSliderButtonPosition(e);

    this.updateParts(mousePos);
    this._rate = this.convertPixelToRate(mousePos);

    this.sliderEventEmitter.emit(
      "slider_change",
      new SliderEventContext(this.rate),
    );
  }

  /**
   * スライダーボタンの位置を制限する関数
   * @return 制限で切り落とされたスライダーボタンの座標値 座標の原点はSliderViewであり、ボタンやバーではない。
   */
  protected limitSliderButtonPosition(evt: PointerEvent): number {
    const mousePos: number = SliderViewUtil.getPointerLocalPosition(
      this,
      this.isHorizontal,
      this.dragStartPos,
      evt,
    );
    return SliderViewUtil.clamp(mousePos, this._maxPosition, this._minPosition);
  }

  /**
   * 各MCの位置、サイズをマウスポインタの位置に合わせて更新する
   * moveSliderの内部処理
   * @param    mousePos SliderViewを原点としたローカルのマウス座標、limitSliderButtonPosition関数で可動範囲に制限済み。
   */
  private updateParts(mousePos: number): void {
    const stretch = (target: Container) => {
      SliderViewUtil.setSize(
        target,
        this.isHorizontal,
        mousePos - SliderViewUtil.getPosition(target, this.isHorizontal),
      );
    };
    //バーマスクがなければ、バー自体を伸縮する
    if (this._bar && !this._barMask) {
      stretch(this._bar);
    }
    //バーマスクがあれば、マスクを伸縮する。
    if (this._barMask) {
      stretch(this._barMask);
    }
    //ボタンの位置を更新する。
    SliderViewUtil.setPosition(this._slideButton, this.isHorizontal, mousePos);
  }

  /**
   * スライダーのドラッグ終了時の処理
   */
  private moveSliderFinish = () => {
    this.isDragging = false;

    SliderViewUtil.removeEventListenerFromTarget(
      this.buttonRootContainer,
      "pointermove",
      this.moveSlider,
    );

    this._slideButton.off("pointerup", this.moveSliderFinish);
    this._slideButton.off("pointerupoutside", this.moveSliderFinish);
    this.sliderEventEmitter.emit(
      "slider_change_finished",
      new SliderEventContext(this.rate),
    );
  };

  /**
   * スライダーの地をクリックした際の処理
   * その位置までスライダーをジャンプする
   * @param evt
   */
  protected onPressBase(evt: PointerEvent): void {
    this.dragStartPos.set(0, 0);
    this.moveSlider(evt);
    this.sliderEventEmitter.emit(
      "slider_change_finished",
      new SliderEventContext(this.rate),
    );
  }

  /**
   * スライダーの割合から、スライダーの位置を取得する
   * @param    rate
   * @return
   */
  protected convertRateToPixel(rate: number): number {
    return SliderViewUtil.convertRateToPixel(
      rate,
      this._maxPosition,
      this._minPosition,
    );
  }

  /**
   * スライダーの座標から、スライダーの割合を取得する
   * @param    pixel
   * @return
   */
  protected convertPixelToRate(pixel: number): number {
    return SliderViewUtil.convertPixelToRate(
      pixel,
      this._maxPosition,
      this._minPosition,
    );
  }

  /**
   * ドラッグ中のマウス座標を取得する。
   * limitSliderButtonPosition内の処理。
   */

  private initBase(value: Container): Container {
    value.eventMode = "static";
    value.on("pointertap", (e) => {
      this.onPressBase(e);
    });
    value.on("wheel", this.onWheel);
    SliderViewUtil.addChildParts(this, value);
    return value;
  }

  private onWheel = (e: FederatedWheelEvent) => {
    const nextRate = SliderViewUtil.clamp(
      this.rate + e.deltaY * this.wheelSpeed,
      SliderView.MAX_RATE,
      0.0,
    );
    this.changeRate(nextRate);
  };

  private initBarAndMask(value?: Container): Container | undefined {
    if (value == null) return;
    value.eventMode = "none";
    SliderViewUtil.addChildParts(this, value);
    return value;
  }

  private initSliderButton(value: Container): Container {
    value.on("pointerdown", this.startMove);
    value.on("wheel", this.onWheel);
    value.eventMode = "static";
    SliderViewUtil.addChildParts(this, value);
    return value;
  }

  /**
   * このインスタンスを破棄する。
   * @param    e
   */
  public dispose(): void {
    this.onDisposeFunction();
  }

  /**
   * 全てのDisplayObjectとEventListenerを解除する。
   */
  protected onDisposeFunction(): void {
    this.removeAllListeners();
    this._base.removeAllListeners();
    this._slideButton.removeAllListeners();
    this.removeChildren();
  }
}