repo_description: Powerful web graphics runtime built on WebGL, WebGPU, WebXR and glTF
repo_topics: 3d-gaussian-splatting game-development game-engine gamedev gaussian-splatting gltf hacktoberfest javascript nodejs playcanvas typescript virtual-reality webgl webgl2 webgpu webxr
code: import { EventHandler } from '../core/event-handler.js';
  import { Vec2 } from '../core/math/vec2.js';
  import { SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from './constants.js';
  import { Mesh } from './mesh.js';
  import { Geometry } from './geometry/geometry.js';
  
  /**
   * @import { GraphicsDevice } from '../platform/graphics/graphics-device.js'
   * @import { TextureAtlas } from './texture-atlas.js'
   */
  
  // normals are the same for every mesh
  const spriteNormals = [
      0, 0, 1,
      0, 0, 1,
      0, 0, 1,
      0, 0, 1
  ];
  
  // indices are the same for every mesh
  const spriteIndices = [
      0, 1, 3,
      2, 3, 1
  ];
  
  /**
   * A Sprite contains references to one or more frames of a {@link TextureAtlas}. It can be used by
   * the {@link SpriteComponent} or the {@link ElementComponent} to render a single frame or a sprite
   * animation.
   *
   * @category Graphics
   */
  class Sprite extends EventHandler {
      /**
       * Create a new Sprite instance.
       *
       * @param {GraphicsDevice} device - The graphics device of the application.
       * @param {object} [options] - Options for creating the Sprite.
       * @param {number} [options.pixelsPerUnit] - The number of pixels that map to one PlayCanvas
       * unit. Defaults to 1.
       * @param {number} [options.renderMode] - The rendering mode of the sprite. Can be:
       *
       * - {@link SPRITE_RENDERMODE_SIMPLE}
       * - {@link SPRITE_RENDERMODE_SLICED}
       * - {@link SPRITE_RENDERMODE_TILED}
       *
       * Defaults to {@link SPRITE_RENDERMODE_SIMPLE}.
       * @param {TextureAtlas} [options.atlas] - The texture atlas. Defaults to null.
       * @param {string[]} [options.frameKeys] - The keys of the frames in the sprite atlas that this
       * sprite is using. Defaults to null.
       */
      constructor(device, options) {
          super();
  
          this._device = device;
          this._pixelsPerUnit = options && options.pixelsPerUnit !== undefined ? options.pixelsPerUnit : 1;
          this._renderMode = options && options.renderMode !== undefined ? options.renderMode : SPRITE_RENDERMODE_SIMPLE;
          this._atlas = options && options.atlas !== undefined ? options.atlas : null;
          this._frameKeys = options && options.frameKeys !== undefined ? options.frameKeys : null;
          this._meshes = [];
  
          // set to true to update multiple
          // properties without re-creating meshes
          this._updatingProperties = false;
          // if true, endUpdate() will re-create meshes when it's called
          this._meshesDirty = false;
  
          if (this._atlas && this._frameKeys) {
              this._createMeshes();
          }
      }
  
      /**
       * Sets the keys of the frames in the sprite atlas that this sprite is using.
       *
       * @type {string[]}
       */
      set frameKeys(value) {
          this._frameKeys = value;
  
          if (this._atlas && this._frameKeys) {
              if (this._updatingProperties) {
                  this._meshesDirty = true;
              } else {
                  this._createMeshes();
              }
          }
  
          this.fire('set:frameKeys', value);
      }
  
      /**
       * Gets the keys of the frames in the sprite atlas that this sprite is using.
       *
       * @type {string[]}
       */
      get frameKeys() {
          return this._frameKeys;
      }
  
      /**
       * Sets the texture atlas.
       *
       * @type {TextureAtlas}
       */
      set atlas(value) {
          if (value === this._atlas) return;
  
          if (this._atlas) {
              this._atlas.off('set:frames', this._onSetFrames, this);
              this._atlas.off('set:frame', this._onFrameChanged, this);
              this._atlas.off('remove:frame', this._onFrameRemoved, this);
          }
  
          this._atlas = value;
          if (this._atlas && this._frameKeys) {
              this._atlas.on('set:frames', this._onSetFrames, this);
              this._atlas.on('set:frame', this._onFrameChanged, this);
              this._atlas.on('remove:frame', this._onFrameRemoved, this);
  
              if (this._updatingProperties) {
                  this._meshesDirty = true;
              } else {
                  this._createMeshes();
              }
          }
  
          this.fire('set:atlas', value);
      }
  
      /**
       * Gets the texture atlas.
       *
       * @type {TextureAtlas}
       */
      get atlas() {
          return this._atlas;
      }
  
      /**
       * Sets the number of pixels that map to one PlayCanvas unit.
       *
       * @type {number}
       */
      set pixelsPerUnit(value) {
          if (this._pixelsPerUnit === value) return;
  
          this._pixelsPerUnit = value;
          this.fire('set:pixelsPerUnit', value);
  
          // simple mode uses pixelsPerUnit to create the mesh so re-create those meshes
          if (this._atlas && this._frameKeys && this.renderMode === SPRITE_RENDERMODE_SIMPLE) {
              if (this._updatingProperties) {
                  this._meshesDirty = true;
              } else {
                  this._createMeshes();
              }
          }
      }
  
      /**
       * Gets the number of pixels that map to one PlayCanvas unit.
       *
       * @type {number}
       */
      get pixelsPerUnit() {
          return this._pixelsPerUnit;
      }
  
      /**
       * Sets the rendering mode of the sprite. Can be:
       *
       * - {@link SPRITE_RENDERMODE_SIMPLE}
       * - {@link SPRITE_RENDERMODE_SLICED}
       * - {@link SPRITE_RENDERMODE_TILED}
       *
       * @type {number}
       */
      set renderMode(value) {
          if (this._renderMode === value) {
              return;
          }
  
          const prev = this._renderMode;
          this._renderMode = value;
          this.fire('set:renderMode', value);
  
          // re-create the meshes if we're going from simple to 9-sliced or vice versa
          if (prev === SPRITE_RENDERMODE_SIMPLE || value === SPRITE_RENDERMODE_SIMPLE) {
              if (this._atlas && this._frameKeys) {
                  if (this._updatingProperties) {
                      this._meshesDirty = true;
                  } else {
                      this._createMeshes();
                  }
              }
          }
      }
  
      /**
       * Sets the rendering mode of the sprite.
       *
       * @type {number}
       */
      get renderMode() {
          return this._renderMode;
      }
  
      /**
       * An array that contains a mesh for each frame.
       *
       * @type {Mesh[]}
       */
      get meshes() {
          return this._meshes;
      }
  
      _createMeshes() {
          // destroy old meshes
          const len = this._meshes.length;
          for (let i = 0; i < len; i++) {
              const mesh = this._meshes[i];
              if (mesh) {
                  mesh.destroy();
              }
          }
  
          // clear meshes array
          const count = this._frameKeys.length;
          this._meshes = new Array(count);
  
          // get function to create meshes
          const createMeshFunc = (this.renderMode === SPRITE_RENDERMODE_SLICED || this._renderMode === SPRITE_RENDERMODE_TILED ? this._create9SliceMesh : this._createSimpleMesh);
  
          // create a mesh for each frame in the sprite
          for (let i = 0; i < count; i++) {
              const frame = this._atlas.frames[this._frameKeys[i]];
              this._meshes[i] = frame ? createMeshFunc.call(this, frame) : null;
          }
  
          this.fire('set:meshes');
      }
  
      _createSimpleMesh(frame) {
          const rect = frame.rect;
          const texWidth = this._atlas.texture.width;
          const texHeight = this._atlas.texture.height;
  
          const w = rect.z / this._pixelsPerUnit;
          const h = rect.w / this._pixelsPerUnit;
          const hp = frame.pivot.x;
          const vp = frame.pivot.y;
  
          // positions based on pivot and size of frame
          const positions = [
              -hp * w,      -vp * h,      0,
              (1 - hp) * w, -vp * h,      0,
              (1 - hp) * w, (1 - vp) * h, 0,
              -hp * w,      (1 - vp) * h, 0
          ];
  
          // uvs based on frame rect
          // uvs
          const lu = rect.x / texWidth;
          const bv = 1.0 - rect.y / texHeight;
          const ru = (rect.x + rect.z) / texWidth;
          const tv = 1.0 - (rect.y + rect.w) / texHeight;
  
          const uvs = [
              lu, bv,
              ru, bv,
              ru, tv,
              lu, tv
          ];
  
          const geom = new Geometry();
          geom.positions = positions;
          geom.normals = spriteNormals;
          geom.uvs = uvs;
          geom.indices = spriteIndices;
  
          return Mesh.fromGeometry(this._device, geom);
      }
  
      _create9SliceMesh() {
          // Check the supplied options and provide defaults for unspecified ones
          const he = Vec2.ONE;
          const ws = 3;
          const ls = 3;
  
          // Variable declarations
          const positions = [];
          const normals = [];
          const uvs = [];
          const indices = [];
  
          // Generate plane as follows (assigned UVs denoted at corners):
          // (0,1)x---------x(1,1)
          //      |         |
          //      |         |
          //      |    O--X |length
          //      |    |    |
          //      |    Z    |
          // (0,0)x---------x(1,0)
          // width
          let vcounter = 0;
          for (let i = 0; i <= ws; i++) {
              const u = (i === 0 || i === ws) ? 0 : 1;
  
              for (let j = 0; j <= ls; j++) {
  
                  const x = -he.x + 2.0 * he.x * (i <= 1 ? 0 : 3) / ws;
                  const y = 0.0;
                  const z = -(-he.y + 2.0 * he.y * (j <= 1 ? 0 : 3) / ls);
  
                  const v = (j === 0 || j === ls) ? 0 : 1;
  
                  positions.push(-x, y, z);
                  normals.push(0.0, 1.0, 0.0);
                  uvs.push(u, v);
  
                  if ((i < ws) && (j < ls)) {
                      indices.push(vcounter + ls + 1, vcounter + 1, vcounter);
                      indices.push(vcounter + ls + 1, vcounter + ls + 2, vcounter + 1);
                  }
  
                  vcounter++;
              }
          }
  
          const geom = new Geometry();
          geom.positions = positions;
          geom.normals = normals;
          geom.uvs = uvs;
          geom.indices = indices;
  
          return Mesh.fromGeometry(this._device, geom);
      }
  
      _onSetFrames(frames) {
          if (this._updatingProperties) {
              this._meshesDirty = true;
          } else {
              this._createMeshes();
          }
      }
  
      _onFrameChanged(frameKey, frame) {
          const idx = this._frameKeys.indexOf(frameKey);
          if (idx < 0) return;
  
          if (frame) {
              // only re-create frame for simple render mode, since
              // 9-sliced meshes don't need frame info to create their mesh
              if (this.renderMode === SPRITE_RENDERMODE_SIMPLE) {
                  this._meshes[idx] = this._createSimpleMesh(frame);
              }
          } else {
              this._meshes[idx] = null;
          }
  
          this.fire('set:meshes');
      }
  
      _onFrameRemoved(frameKey) {
          const idx = this._frameKeys.indexOf(frameKey);
          if (idx < 0) return;
  
          this._meshes[idx] = null;
          this.fire('set:meshes');
      }
  
      startUpdate() {
          this._updatingProperties = true;
          this._meshesDirty = false;
      }
  
      endUpdate() {
          this._updatingProperties = false;
          if (this._meshesDirty && this._atlas && this._frameKeys) {
              this._createMeshes();
  
          }
          this._meshesDirty = false;
      }
  
      /**
       * Free up the meshes created by the sprite.
       */
      destroy() {
          for (const mesh of this._meshes) {
              if (mesh) {
                  mesh.destroy();
              }
          }
          this._meshes.length = 0;
      }
  }
  
  export { Sprite };
  