Source: src/SpinViewer/SpriteImage.ts

import Component, { ComponentEvent } from "@egjs/component";

import { TRANSFORM, SUPPORT_WILLCHANGE } from "../utils/browserFeature";
import { VERSION } from "../version";

import { SpinViewerOptions } from "./SpinViewer";
import { DEFAULT_IMAGE_CLASS, DEFAULT_WRAPPER_CLASS } from "./consts";

export interface SpriteImageEvent {
  /**
   * Events that occur when component loading is complete
   * @ko 컴포넌트 로딩이 완료되면 발생하는 이벤트
   * @name eg.view360.SpriteImage#load
   * @event
   * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
   * @param {HTMLElement} param.target The target element for which to display the image <ko>이미지를 보여줄 대상 엘리먼트</ko>
   * @param {HTMLElement} param.bgElement Generated background image element <ko>생성된 background 이미지 엘리먼트</ko>
   *
   * @example
   *
   * sprites.on({
   *  "load" : function(evt) {
   *    console.log("load event fired - e.target", e.target, "e.bgElement", e.bgElement);
   *  }
   * });
   */
  load: {
    target: HTMLElement;
    bgElement: HTMLDivElement;
  };
  /**
   * An event that occurs when the image index is changed by the user's left / right panning
   * @ko 사용자의 좌우 Panning 에 의해 이미지 인덱스가 변경되었을때 발생하는 이벤트
   * @name eg.view360.SpriteImage#imageError
   * @event
   * @param {Object} param The object of data to be sent to an event <ko>이벤트에 전달되는 데이터 객체</ko>
   * @param {String} param.imageUrl User-specified image URL <ko>사용자가 지정한 이미지 URL</ko>
   *
   * @example
   *
   * sprites.on({
   *  "imageError" : function(evt) {
   *    // Error handling
   *    console.log(e.imageUrl);
   *  }
   * });
   */
  imageError: {
    imageUrl?: string;
  };
}

/**
 * @memberof eg.view360
 * @extends eg.Component
 * SpriteImage
 */
class SpriteImage extends Component<SpriteImageEvent> {
  private static _createBgDiv(wrapperInContainer: HTMLDivElement | null, img: HTMLImageElement, rowCount: number, colCount: number, autoHeight: boolean) {
    const el = wrapperInContainer || document.createElement("div");

    el.style.position = "relative";
    el.style.overflow = "hidden";

    img.style.position = "absolute";
    img.style.width = `${colCount * 100}%`;
    img.style.height = `${rowCount * 100}%`;

    /** Prevent image from being dragged on IE10, IE11, Safari especially */
    img.ondragstart = () => (false); // img.style.pointerEvents = "none";
    // Use hardware accelerator if available
    if (SUPPORT_WILLCHANGE) {
      (img.style.willChange = "transform");
    }

    el.appendChild(img);

    const unitWidth = img.naturalWidth / colCount;
    const unitHeight = img.naturalHeight / rowCount;

    if (autoHeight) {
      const r = unitHeight / unitWidth;

      el.style.paddingBottom = `${r * 100}%`;
    } else {
      el.style.height = "100%";
    }

    return el;
  }

  private static _getSizeString(size) {
    if (typeof size === "number") {
      return `${size}px`;
    }

    return size;
  }

  public static VERSION = VERSION;

  private _el: HTMLElement;
  private _rowCount: number;
  private _colCount: number;
  private _totalCount: number;
  private _width: number | string;
  private _height: number | string;
  private _autoHeight: boolean;
  private _colRow: number[];
  private _image: HTMLImageElement;
  private _bg: HTMLDivElement;
  private _autoPlayReservedInfo: { interval: number; playCount: number } | null;
  private _autoPlayTimer: number;

  /**
   * @class eg.view360.SpriteImage
   * @classdesc A module that displays a single or continuous image of any one of the "sprite images". SpinViewer internally uses SpriteImage to show each frame of the sprite image.
   * @ko 스프라이트 이미지 중 임의의 한 프레임을 단발성 혹은 연속적으로 보여주는 컴포넌트입니다. SpinViewer 는 내부적으로 SpriteImage 를 사용하여 스프라이트 이미지의 각 프레임을 보여줍니다.
   * @extends eg.Component
   *
   * @param {HTMLElement} element The element to show the image <ko>이미지를 보여줄 대상 요소</ko>
   * @param {Object} options The option object<ko>파라미터 객체</ko>
   * @param {String} options.imageUrl The url of the sprite image <ko>스프라이트 이미지의 url</ko>
   * @param {Number} [options.rowCount=1] Number of horizontal frames in the sprite image <ko>스프라이트 이미지의 가로 프레임 갯수</ko>
   * @param {Number} [options.colCount=1] Number of vertical frames in the sprite image <ko>스프라이트 이미지의 세로 프레임 갯수</ko>
   * @param {Number|String} [options.width="auto"] The width of the target element to show the image <ko>이미지를 보여줄 대상 요소의 너비</ko>
   * @param {Number|String} [options.height="auto"] The height of the target element to show the image <ko>이미지를 보여줄 대상 요소의 높이</ko>
   * @param {Boolean} [options.autoHeight=true] Whether to automatically set the height of the image area to match the original image's proportion <ko>원본 이미지 비율에 맞게 이미지 영역의 높이를 자동으로 설정할지 여부</ko>
   * @param {Number[]} [options.colRow=[0, 0]] The column, row coordinates of the first frame of the sprite image (based on 0 index) <ko> 스프라이트 이미지 중 처음 보여줄 프레임의 (column, row) 좌표 (0 index 기반)</ko>
   * @param {Number} [options.frameIndex=0] frameIndex specifies the index of the frame to be displayed in the "Sprite image". The frameIndex order is zero-based and indexed in Z form (left-to-right, top-to-bottom, and newline again from left to right).<br>- colRow is equivalent to frameIndex. However, if colRow is specified at the same time, colRow takes precedence.<ko>스프라이트 이미지 중에서 보여질 프레임의 인덱스를 지정합니다. frameIndex 순서는 0부터 시작하며 Z 형태(왼쪽에서 오른쪽, 위에서 아래, 개행 시 다시 왼쪽 부터)로 인덱싱합니다.<br>- colRow 는 frameIndex 와 동일한 기능을 합니다. 단, colRow 가 동시에 지정된 경우 colRow 가 우선합니다.</ko>
   * @param {Number} [options.scale=1] Spin scale (The larger the spin, the more).<ko>Spin 배율 (클 수록 더 많이 움직임)</ko>
   *
   * @support {"ie": "9+", "ch" : "latest", "ff" : "latest",  "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
   * @example
   *
   * // Initialize SpriteImage
   *
   * var el = document.getElementById("image-div");
   * var sprites = new eg.view360.SpriteImage(el, {
   * 	imageUrl: "/img/bag360.jpg", // required
   * 	rowCount: 24
   * });
   */
  public constructor(element: HTMLElement, options: Partial<SpinViewerOptions> = {}) {
    super();
    const opt = options || {};

    this._el = element;
    this._rowCount = opt.rowCount || 1;
    this._colCount = opt.colCount || 1;
    this._totalCount = this._rowCount * this._colCount; // total frames
    this._width = opt.width || "auto";
    this._height = opt.height || "auto";
    this._autoHeight = opt.autoHeight != null ? opt.autoHeight : true; // If autoHeight is specified, _height will be overwritten.
    this._colRow = [0, 0];

    if (opt.colRow) {
      this._colRow = opt.colRow;
    } else if (opt.frameIndex) {
      this.setFrameIndex(opt.frameIndex);
    }

    this._el.style.width = SpriteImage._getSizeString(this._width);
    this._el.style.height = SpriteImage._getSizeString(this._height);

    const wrapperClass = opt.wrapperClass || DEFAULT_WRAPPER_CLASS;
    const imageClass = opt.imageClass || DEFAULT_IMAGE_CLASS;

    if (!opt.imageUrl) {
      setTimeout(() => {
        this.trigger(new ComponentEvent("imageError", {
          imageUrl: opt.imageUrl
        }));
      }, 0);
      return;
    }

    const imageInContainer = element.querySelector<HTMLImageElement>(`.${imageClass}`);
    const wrapperInContainer = element.querySelector<HTMLDivElement>(`.${wrapperClass}`);

    if (wrapperInContainer && imageInContainer) {
      // Set it to invisible to prevent wrapper being resized
      imageInContainer.style.display = "none";
    }

    this._image = imageInContainer || new Image();
    /**
     * Event
     */

    const image = this._image;

    image.onload = () => {
      if (wrapperInContainer && imageInContainer) {
        imageInContainer.style.display = "";
      }

      this._bg = SpriteImage._createBgDiv(
        wrapperInContainer,
        image,
        this._rowCount,
        this._colCount,
        this._autoHeight
      );
      this._el.appendChild(this._bg);
      this.setColRow(this._colRow[0], this._colRow[1]);

      this.trigger(new ComponentEvent("load", {
        target: this._el,
        bgElement: this._bg
      }));

      if (this._autoPlayReservedInfo) {
        this.play(this._autoPlayReservedInfo);
        this._autoPlayReservedInfo = null;
      }
    };

    image.onerror = () => {
      this.trigger(new ComponentEvent("imageError", {
        imageUrl: opt.imageUrl
      }));
    };

    image.src = opt.imageUrl;
  }

  /**
   * Specifies the frameIndex of the frame to be shown in the sprite image.
   * @ko 스프라이트 이미지 중 보여질 프레임의 frameIndex 값을 지정
   * @method eg.view360.SpriteImage#setFrameIndex
   * @param {Number} frameIndex frame index of a frame<ko>프레임의 인덱스</ko>
   *
   * @example
   *
   * sprites.setFrameIndex(0, 1);// col = 0, row = 1
   */
  public setFrameIndex(index: number) {
    const colRow = this.toColRow(index);

    this.setColRow(colRow[0], colRow[1]);
  }

  /**
   * Returns the frameIndex of the frame to be shown in the sprite image.
   * @ko 스프라이트 이미지 중 보여지는 프레임의 index 값을 반환
   * @method eg.view360.SpriteImage#getFrameIndex
   * @return {Number} frame index <ko>frame 인덱스</ko>
   *
   * @example
   *
   * var frameIndex = sprites.getFrameIndex(); // eg. frameIndex = 1
   *
   */
  public getFrameIndex() {
    return this._colRow[1] * this._colCount + this._colRow[0];
  }

  /**
   * Specifies the col and row values of the frame to be shown in the sprite image.
   * @ko 스프라이트 이미지 중 보여질 프레임의 col, row 값을 지정
   * @method eg.view360.SpriteImage#setColRow
   * @param {Number} col Column number of a frame<ko>프레임의 행값</ko>
   * @param {Number} row Row number of a frame<ko>프레임의 열값</ko>
   *
   * @example
   *
   * sprites.setlColRow(1, 2); // col = 1, row = 2
   */
  public setColRow(col: number, row: number) {
    if (row > this._rowCount - 1 || col > this._colCount - 1) {
      return;
    }

    if (this._image && TRANSFORM) {
      // NOTE: Currently, do not apply translate3D for using layer hack. Do we need layer hack for old browser?
      this._image.style[TRANSFORM] = `translate(${-(col / this._colCount * 100)}%, ${-(row / this._rowCount * 100)}%)`;
    }

    this._colRow = [col, row];
  }

  /**
   * Returns the col and row values of the frame to be shown in the sprite image.
   * @ko 스프라이트 이미지 중 보여지는 프레임의 col, row 값을환반환
   * @method eg.view360.SpriteImage#gelColRow
   * @return {Number[]} Array containing col, row<ko>col, row 정보를 담는 배열</ko>
   *
   * @example
   *
   * var colRow = sprites.getlColRow();
   * // colRow = [1, 2] - index of col is 1, index of row is 2
   *
   */
  public getColRow() {
    return this._colRow;
  }

  /**
   * Stop playing
   * @ko play 되고 있던 프레임 재생을 중지합니다.
   * @method eg.view360.SpriteImage#stop
   *
   * @example
   *
   * viewer.stop();
   *
   */
  public stop() {
    if (this._autoPlayTimer) {
      clearInterval(this._autoPlayTimer);
      this._autoPlayTimer = -1;
    }
  }

  /**
   * Switches frames sequentially in the 'interval' starting from the currently displayed frame and plays all frames by 'playCount'.
   * @ko 현재 보여지고 있는 프레임을 시작으로 'interval' 간격으로 순차적으로 프레임을 전환하며 모든 프레임을 'playCount' 만큼 재생한다.
   * @method eg.view360.SpriteImage#play
   * @param {Object} param The parameter object<ko>파라미터 객체</ko>
   * @param {Number} [param.interval=1000 / totalFrameCount] Interframe Interval - in milliseconds<ko>프레임간 간격 - 밀리세컨드 단위</ko>
   * @param {Number} [param.playCount=0] PlayCount = 1 in which all frames are reproduced once, and playCount = n in which all frames are repeated n times. playCount = 0 in which all frames are repeated infinitely<ko>모든 프레임을 1회씩 재생한 것이 playCount = 1, 모든 프레임을 n 회 재상한 것이 playCount = n 이 된다. 0 dms 무한반복</ko>
   *
   * @example
   *
   * viewer.play({angle: 16, playCount: 1});
   *
   */
  public play({ interval, playCount } = { interval: 1000 / this._totalCount, playCount: 0 }) {
    if (!this._bg) {
      this._autoPlayReservedInfo = {interval, playCount};
      return;
    }

    if (this._autoPlayTimer) {
      clearInterval(this._autoPlayTimer);
      this._autoPlayTimer = -1;
    }

    let frameIndex = this.getFrameIndex();
    let count = 0;
    let frameCount = 0; // for checking 1 cycle

    this._autoPlayTimer = window.setInterval(() => {
      frameIndex %= this._totalCount;
      const colRow = this.toColRow(frameIndex);

      this.setColRow(colRow[0], colRow[1]);
      frameIndex++;

      // Done 1 Cycle?
      if (++frameCount === this._totalCount) {
        frameCount = 0;
        count++;
      }

      if (playCount > 0 && count === playCount) {
        clearInterval(this._autoPlayTimer);
      }
    }, interval);
  }

  public toColRow(frameIndex: number) {
    const colCount = this._colCount;
    const rowCount = this._rowCount;

    if (frameIndex < 0) {
      return [0, 0];
    } else if (frameIndex >= this._totalCount) {
      return [colCount - 1, rowCount - 1];
    }

    const col = frameIndex % colCount;
    const row = Math.floor(frameIndex / colCount);

    // console.log(frameIndex, col, row);
    return [col, row];
  }
}

export default SpriteImage;
comments powered by Disqus