Source: tiny/core/textures/Spritesheet.js

import Rectangle from '../math/shapes/Rectangle';
import Texture from './Texture';
import { getResolutionOfUrl } from '../../../utils';

/**
 * Utility class for maintaining reference to a collection of Textures on a single Spritesheet.
 *
 * @class
 * @memberof Tiny
 */
export default class Spritesheet {
  /**
   * The maximum number of Textures to build per process.
   *
   * @type {number}
   * @default 1000
   */
  static get BATCH_SIZE() {
    return 1000;
  }

  /**
   * @param {Tiny.BaseTexture} baseTexture - Reference to the source BaseTexture object.
   * @param {object} data - Spritesheet image data.
   * @param {string} [resolutionFilename] - The filename to consider when determining the resolution of the spritesheet. If not provided, the imageUrl will be used on the BaseTexture.
   */
  constructor(baseTexture, data, resolutionFilename = null) {
    /**
     * Reference to ths source texture
     * @type {Tiny.BaseTexture}
     */
    this.baseTexture = baseTexture;

    /**
     * A map containing all textures of the sprite sheet.
     * Can be used to create a {@link Tiny.Sprite}:
     * ```js
     * new Tiny.Sprite(sheet.textures['image.png']);
     * ```
     *
     * @member {object}
     */
    this.textures = {};

    /**
     * A map containing the textures for each animation.
     * Can be used to create an {@link Tiny.AnimatedSprite}:
     * ```js
     * new Tiny.AnimatedSprite(sheet.animations['anim_name'])
     * ```
     *
     * @version 1.2.0
     * @member {object}
     */
    this.animations = {};

    /**
     * Reference to the original JSON data.
     *
     * @type {object}
     */
    this.data = data;

    /**
     * The resolution of the spritesheet.
     *
     * @type {number}
     */
    this.resolution = this._updateResolution(
      resolutionFilename || this.baseTexture.imageUrl
    );

    /**
     * Map of spritesheet frames.
     *
     * @type {object}
     * @private
     */
    this._frames = this.data.frames;

    /**
     * Collection of frame names.
     *
     * @type {string[]}
     * @private
     */
    this._frameKeys = Object.keys(this._frames);

    /**
     * Current batch index being processed.
     *
     * @type {number}
     * @private
     */
    this._batchIndex = 0;

    /**
     * Callback when parse is completed.
     *
     * @type {function}
     * @private
     */
    this._callback = null;
  }

  /**
   * Generate the resolution from the filename or fallback to the meta.scale field of the JSON data.
   *
   * @private
   * @param {string} resolutionFilename - The filename to use for resolving the default resolution.
   * @return {number} Resolution to use for spritesheet.
   */
  _updateResolution(resolutionFilename) {
    const scale = this.data.meta.scale;

    // Use a defaultValue of `null` to check if a url-based resolution is set
    let resolution = getResolutionOfUrl(resolutionFilename, null);

    // No resolution found via URL
    if (resolution === null) {
      // Use the scale value or default to 1
      resolution = scale !== undefined ? parseFloat(scale) : 1;
    }

    // For non-1 resolutions, update baseTexture
    if (resolution !== 1) {
      this.baseTexture.resolution = resolution;
      this.baseTexture.update();
    }

    return resolution;
  }

  /**
   * Parser spritesheet from loaded data. This is done asynchronously to prevent creating too many Texture within a single process.
   *
   * @param {function} callback - Callback when complete returns a map of the Textures for this spritesheet.
   */
  parse(callback) {
    this._batchIndex = 0;
    this._callback = callback;

    if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) {
      this._processFrames(0);
      this._processAnimations();
      this._parseComplete();
    } else {
      this._nextBatch();
    }
  }

  /**
   * Process a batch of frames
   *
   * @private
   * @param {number} initialFrameIndex - The index of frame to start.
   */
  _processFrames(initialFrameIndex) {
    let frameIndex = initialFrameIndex;
    const maxFrames = Spritesheet.BATCH_SIZE;
    const sourceScale = this.baseTexture.sourceScale;

    while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) {
      const i = this._frameKeys[frameIndex];
      const data = this._frames[i];
      const rect = data.frame;

      if (rect) {
        let frame = null;
        let trim = null;
        const sourceSize = data.trimmed !== false && data.sourceSize ? data.sourceSize : data.frame;

        const orig = new Rectangle(
          0,
          0,
          Math.floor(sourceSize.w * sourceScale) / this.resolution,
          Math.floor(sourceSize.h * sourceScale) / this.resolution
        );

        if (data.rotated) {
          frame = new Rectangle(
            Math.floor(rect.x * sourceScale) / this.resolution,
            Math.floor(rect.y * sourceScale) / this.resolution,
            Math.floor(rect.h * sourceScale) / this.resolution,
            Math.floor(rect.w * sourceScale) / this.resolution
          );
        } else {
          frame = new Rectangle(
            Math.floor(rect.x * sourceScale) / this.resolution,
            Math.floor(rect.y * sourceScale) / this.resolution,
            Math.floor(rect.w * sourceScale) / this.resolution,
            Math.floor(rect.h * sourceScale) / this.resolution
          );
        }

        //  Check to see if the sprite is trimmed
        if (data.trimmed !== false && data.spriteSourceSize) {
          trim = new Rectangle(
            Math.floor(data.spriteSourceSize.x * sourceScale) / this.resolution,
            Math.floor(data.spriteSourceSize.y * sourceScale) / this.resolution,
            Math.floor(rect.w * sourceScale) / this.resolution,
            Math.floor(rect.h * sourceScale) / this.resolution
          );
        }

        this.textures[i] = new Texture(
          this.baseTexture,
          frame,
          orig,
          trim,
          data.rotated ? 2 : 0,
          data.anchor
        );

        // lets also add the frame to the global cache for fromFrame and fromImage functions
        Texture.addToCache(this.textures[i], i);
      }

      frameIndex++;
    }
  }

  /**
   * Parse animations config
   *
   * @private
   * @version 1.2.0
   */
  _processAnimations() {
    const animations = this.data.animations || {};

    for (const animName in animations) {
      this.animations[animName] = [];
      for (const frameName of animations[animName]) {
        this.animations[animName].push(this.textures[frameName]);
      }
    }
  }

  /**
   * The parse has completed.
   *
   * @private
   */
  _parseComplete() {
    const callback = this._callback;

    this._callback = null;
    this._batchIndex = 0;
    callback.call(this, this.textures);
  }

  /**
   * Begin the next batch of textures.
   *
   * @private
   */
  _nextBatch() {
    this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE);
    this._batchIndex++;
    setTimeout(() => {
      if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) {
        this._nextBatch();
      } else {
        this._processAnimations();
        this._parseComplete();
      }
    }, 0);
  }

  /**
   * Destroy Spritesheet and don't use after this.
   *
   * @param {boolean} [destroyBase=false] - Whether to destroy the base texture as well
   */
  destroy(destroyBase = false) {
    for (const i in this.textures) {
      this.textures[i].destroy();
    }
    this._frames = null;
    this._frameKeys = null;
    this.data = null;
    this.textures = null;
    if (destroyBase) {
      this.baseTexture.destroy();
    }
    this.baseTexture = null;
  }
}
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:25 GMT+0800 (CST)