playcanvas
Version:
PlayCanvas WebGL game engine
354 lines (351 loc) • 11.6 kB
JavaScript
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 };