playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
594 lines (593 loc) • 17.1 kB
JavaScript
import { math } from "../../../core/math/math.js";
import { Color } from "../../../core/math/color.js";
import { Vec2 } from "../../../core/math/vec2.js";
import { Vec4 } from "../../../core/math/vec4.js";
import {
LAYERID_WORLD,
SPRITE_RENDERMODE_SLICED,
SPRITE_RENDERMODE_TILED
} from "../../../scene/constants.js";
import { BatchGroup } from "../../../scene/batching/batch-group.js";
import { GraphNode } from "../../../scene/graph-node.js";
import { MeshInstance } from "../../../scene/mesh-instance.js";
import { Component } from "../component.js";
import { SPRITETYPE_SIMPLE, SPRITETYPE_ANIMATED } from "./constants.js";
import { SpriteAnimationClip } from "./sprite-animation-clip.js";
const PARAM_EMISSIVE_MAP = "texture_emissiveMap";
const PARAM_OPACITY_MAP = "texture_opacityMap";
const PARAM_EMISSIVE = "material_emissive";
const PARAM_OPACITY = "material_opacity";
const PARAM_INNER_OFFSET = "innerOffset";
const PARAM_OUTER_SCALE = "outerScale";
const PARAM_ATLAS_RECT = "atlasRect";
class SpriteComponent extends Component {
static EVENT_PLAY = "play";
static EVENT_PAUSE = "pause";
static EVENT_RESUME = "resume";
static EVENT_STOP = "stop";
static EVENT_END = "end";
static EVENT_LOOP = "loop";
_evtLayersChanged = null;
_evtLayerAdded = null;
_evtLayerRemoved = null;
_type = SPRITETYPE_SIMPLE;
_material = null;
_color = new Color(1, 1, 1, 1);
_colorUniform = new Float32Array(3);
_speed = 1;
_flipX = false;
_flipY = false;
_width = 1;
_height = 1;
_drawOrder = 0;
_layers = [LAYERID_WORLD];
// assign to the default world layer
// 9-slicing
_outerScale = new Vec2(1, 1);
_outerScaleUniform = new Float32Array(2);
_innerOffset = new Vec4();
_innerOffsetUniform = new Float32Array(4);
_atlasRect = new Vec4();
_atlasRectUniform = new Float32Array(4);
// batch groups
_batchGroupId = -1;
// node / mesh instance
_node = new GraphNode();
_meshInstance = null;
_updateAabbFunc = null;
_inLayers = false;
// animated sprites
_autoPlayClip = null;
_clips = {};
_defaultClip = null;
_currentClip = null;
constructor(system, entity) {
super(system, entity);
this._material = system.defaultMaterial;
entity.addChild(this._node);
this._updateAabbFunc = this._updateAabb.bind(this);
this._defaultClip = new SpriteAnimationClip(this, {
name: this.entity.name,
fps: 0,
loop: false,
spriteAsset: null
});
this._currentClip = this._defaultClip;
}
set type(value) {
if (this._type === value) {
return;
}
this._type = value;
if (this._type === SPRITETYPE_SIMPLE) {
this.stop();
this._currentClip = this._defaultClip;
if (this.enabled && this.entity.enabled) {
this._currentClip.frame = this.frame;
if (this._currentClip.sprite) {
this.addToLayers();
} else {
this.removeFromLayers();
}
}
} else if (this._type === SPRITETYPE_ANIMATED) {
this.stop();
if (this._autoPlayClip) {
this._tryAutoPlay();
}
if (this._currentClip && this._currentClip.isPlaying && this.enabled && this.entity.enabled) {
this.addToLayers();
} else {
this.removeFromLayers();
}
}
}
get type() {
return this._type;
}
set frame(value) {
this._currentClip.frame = value;
}
get frame() {
return this._currentClip.frame;
}
set spriteAsset(value) {
this._defaultClip.spriteAsset = value;
}
get spriteAsset() {
return this._defaultClip._spriteAsset;
}
set sprite(value) {
this._currentClip.sprite = value;
}
get sprite() {
return this._currentClip.sprite;
}
// (private) {pc.Material} material The material used to render a sprite.
set material(value) {
this._material = value;
if (this._meshInstance) {
this._meshInstance.material = value;
}
}
get material() {
return this._material;
}
set color(value) {
this._color.r = value.r;
this._color.g = value.g;
this._color.b = value.b;
if (this._meshInstance) {
this._colorUniform[0] = this._color.r;
this._colorUniform[1] = this._color.g;
this._colorUniform[2] = this._color.b;
this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform);
}
}
get color() {
return this._color;
}
set opacity(value) {
this._color.a = value;
if (this._meshInstance) {
this._meshInstance.setParameter(PARAM_OPACITY, value);
}
}
get opacity() {
return this._color.a;
}
set clips(value) {
if (!value) {
for (const name in this._clips) {
this.removeClip(name);
}
return;
}
for (const name in this._clips) {
let found = false;
for (const key in value) {
if (value[key].name === name) {
found = true;
this._clips[name].fps = value[key].fps;
this._clips[name].loop = value[key].loop;
if (value[key].hasOwnProperty("sprite")) {
this._clips[name].sprite = value[key].sprite;
} else if (value[key].hasOwnProperty("spriteAsset")) {
this._clips[name].spriteAsset = value[key].spriteAsset;
}
break;
}
}
if (!found) {
this.removeClip(name);
}
}
for (const key in value) {
if (this._clips[value[key].name]) continue;
this.addClip(value[key]);
}
if (this._autoPlayClip) {
this._tryAutoPlay();
}
if (!this._currentClip || !this._currentClip.sprite) {
this.removeFromLayers();
}
}
get clips() {
return this._clips;
}
get currentClip() {
return this._currentClip;
}
set speed(value) {
this._speed = value;
}
get speed() {
return this._speed;
}
set flipX(value) {
if (this._flipX === value) return;
this._flipX = value;
this._updateTransform();
}
get flipX() {
return this._flipX;
}
set flipY(value) {
if (this._flipY === value) return;
this._flipY = value;
this._updateTransform();
}
get flipY() {
return this._flipY;
}
set width(value) {
if (value === this._width) return;
this._width = value;
this._outerScale.x = this._width;
if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_TILED || this.sprite.renderMode === SPRITE_RENDERMODE_SLICED)) {
this._updateTransform();
}
}
get width() {
return this._width;
}
set height(value) {
if (value === this._height) return;
this._height = value;
this._outerScale.y = this.height;
if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_TILED || this.sprite.renderMode === SPRITE_RENDERMODE_SLICED)) {
this._updateTransform();
}
}
get height() {
return this._height;
}
set batchGroupId(value) {
if (this._batchGroupId === value) {
return;
}
const prev = this._batchGroupId;
this._batchGroupId = value;
if (this.entity.enabled && prev >= 0) {
this.system.app.batcher?.remove(BatchGroup.SPRITE, prev, this.entity);
}
if (this.entity.enabled && value >= 0) {
this.system.app.batcher?.insert(BatchGroup.SPRITE, value, this.entity);
} else {
if (prev >= 0) {
if (this._currentClip && this._currentClip.sprite && this.enabled && this.entity.enabled) {
this.addToLayers();
}
}
}
}
get batchGroupId() {
return this._batchGroupId;
}
set autoPlayClip(value) {
this._autoPlayClip = value instanceof SpriteAnimationClip ? value.name : value;
this._tryAutoPlay();
}
get autoPlayClip() {
return this._autoPlayClip;
}
set drawOrder(value) {
this._drawOrder = value;
if (this._meshInstance) {
this._meshInstance.drawOrder = value;
}
}
get drawOrder() {
return this._drawOrder;
}
set layers(value) {
if (this._inLayers) {
this.removeFromLayers();
}
this._layers = value;
if (!this._meshInstance) {
return;
}
if (this.enabled && this.entity.enabled) {
this.addToLayers();
}
}
get layers() {
return this._layers;
}
get aabb() {
if (this._meshInstance) {
return this._meshInstance.aabb;
}
return null;
}
onEnable() {
const app = this.system.app;
const scene = app.scene;
const layers = scene.layers;
this._evtLayersChanged = scene.on("set:layers", this._onLayersChanged, this);
if (layers) {
this._evtLayerAdded = layers.on("add", this._onLayerAdded, this);
this._evtLayerRemoved = layers.on("remove", this._onLayerRemoved, this);
}
this.addToLayers();
if (this._autoPlayClip) {
this._tryAutoPlay();
}
if (this._batchGroupId >= 0) {
app.batcher?.insert(BatchGroup.SPRITE, this._batchGroupId, this.entity);
}
}
onDisable() {
const app = this.system.app;
const scene = app.scene;
const layers = scene.layers;
this._evtLayersChanged?.off();
this._evtLayersChanged = null;
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = null;
this._evtLayerRemoved?.off();
this._evtLayerRemoved = null;
}
this.stop();
this.removeFromLayers();
if (this._batchGroupId >= 0) {
app.batcher?.remove(BatchGroup.SPRITE, this._batchGroupId, this.entity);
}
}
onDestroy() {
this._currentClip = null;
if (this._defaultClip) {
this._defaultClip._destroy();
this._defaultClip = null;
}
for (const key in this._clips) {
this._clips[key]._destroy();
}
this._clips = null;
this.removeFromLayers();
this._node?.remove();
this._node = null;
if (this._meshInstance) {
this._meshInstance.material = null;
this._meshInstance.mesh = null;
this._meshInstance = null;
}
}
addToLayers() {
if (this._inLayers) return;
if (!this._meshInstance) return;
const meshInstances = [this._meshInstance];
for (let i = 0, len = this._layers.length; i < len; i++) {
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
layer.addMeshInstances(meshInstances);
}
}
this._inLayers = true;
}
removeFromLayers() {
if (!this._inLayers || !this._meshInstance) return;
const meshInstances = [this._meshInstance];
for (let i = 0, len = this._layers.length; i < len; i++) {
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
layer.removeMeshInstances(meshInstances);
}
}
this._inLayers = false;
}
// Set the desired mesh on the mesh instance
_showFrame(frame) {
if (!this.sprite) return;
const mesh = this.sprite.meshes[frame];
if (!mesh) {
if (this._meshInstance) {
this._meshInstance.mesh = null;
this._meshInstance.visible = false;
}
return;
}
let material;
if (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED) {
material = this.system.default9SlicedMaterialSlicedMode;
} else if (this.sprite.renderMode === SPRITE_RENDERMODE_TILED) {
material = this.system.default9SlicedMaterialTiledMode;
} else {
material = this.system.defaultMaterial;
}
if (!this._meshInstance) {
this._meshInstance = new MeshInstance(mesh, this._material, this._node);
this._meshInstance.castShadow = false;
this._meshInstance.receiveShadow = false;
this._meshInstance.drawOrder = this._drawOrder;
this._colorUniform[0] = this._color.r;
this._colorUniform[1] = this._color.g;
this._colorUniform[2] = this._color.b;
this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform);
this._meshInstance.setParameter(PARAM_OPACITY, this._color.a);
if (this.enabled && this.entity.enabled) {
this.addToLayers();
}
}
if (this._meshInstance.material !== material) {
this._meshInstance.material = material;
}
if (this._meshInstance.mesh !== mesh) {
this._meshInstance.mesh = mesh;
this._meshInstance.visible = true;
this._meshInstance._aabbVer = -1;
}
if (this.sprite.atlas && this.sprite.atlas.texture) {
this._meshInstance.setParameter(PARAM_EMISSIVE_MAP, this.sprite.atlas.texture);
this._meshInstance.setParameter(PARAM_OPACITY_MAP, this.sprite.atlas.texture);
} else {
this._meshInstance.deleteParameter(PARAM_EMISSIVE_MAP);
this._meshInstance.deleteParameter(PARAM_OPACITY_MAP);
}
if (this.sprite.atlas && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED)) {
this._meshInstance._updateAabbFunc = this._updateAabbFunc;
const frameData = this.sprite.atlas.frames[this.sprite.frameKeys[frame]];
if (frameData) {
const borderWidthScale = 2 / frameData.rect.z;
const borderHeightScale = 2 / frameData.rect.w;
this._innerOffset.set(
frameData.border.x * borderWidthScale,
frameData.border.y * borderHeightScale,
frameData.border.z * borderWidthScale,
frameData.border.w * borderHeightScale
);
const tex = this.sprite.atlas.texture;
this._atlasRect.set(
frameData.rect.x / tex.width,
frameData.rect.y / tex.height,
frameData.rect.z / tex.width,
frameData.rect.w / tex.height
);
} else {
this._innerOffset.set(0, 0, 0, 0);
}
this._innerOffsetUniform[0] = this._innerOffset.x;
this._innerOffsetUniform[1] = this._innerOffset.y;
this._innerOffsetUniform[2] = this._innerOffset.z;
this._innerOffsetUniform[3] = this._innerOffset.w;
this._meshInstance.setParameter(PARAM_INNER_OFFSET, this._innerOffsetUniform);
this._atlasRectUniform[0] = this._atlasRect.x;
this._atlasRectUniform[1] = this._atlasRect.y;
this._atlasRectUniform[2] = this._atlasRect.z;
this._atlasRectUniform[3] = this._atlasRect.w;
this._meshInstance.setParameter(PARAM_ATLAS_RECT, this._atlasRectUniform);
} else {
this._meshInstance._updateAabbFunc = null;
}
this._updateTransform();
}
_updateTransform() {
let scaleX = this.flipX ? -1 : 1;
let scaleY = this.flipY ? -1 : 1;
let posX = 0;
let posY = 0;
if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED)) {
let w = 1;
let h = 1;
if (this.sprite.atlas) {
const frameData = this.sprite.atlas.frames[this.sprite.frameKeys[this.frame]];
if (frameData) {
w = frameData.rect.z;
h = frameData.rect.w;
posX = (0.5 - frameData.pivot.x) * this._width;
posY = (0.5 - frameData.pivot.y) * this._height;
}
}
const scaleMulX = w / this.sprite.pixelsPerUnit;
const scaleMulY = h / this.sprite.pixelsPerUnit;
this._outerScale.set(Math.max(this._width, this._innerOffset.x * scaleMulX), Math.max(this._height, this._innerOffset.y * scaleMulY));
scaleX *= scaleMulX;
scaleY *= scaleMulY;
this._outerScale.x /= scaleMulX;
this._outerScale.y /= scaleMulY;
scaleX *= math.clamp(this._width / (this._innerOffset.x * scaleMulX), 1e-4, 1);
scaleY *= math.clamp(this._height / (this._innerOffset.y * scaleMulY), 1e-4, 1);
if (this._meshInstance) {
this._outerScaleUniform[0] = this._outerScale.x;
this._outerScaleUniform[1] = this._outerScale.y;
this._meshInstance.setParameter(PARAM_OUTER_SCALE, this._outerScaleUniform);
}
}
this._node.setLocalScale(scaleX, scaleY, 1);
this._node.setLocalPosition(posX, posY, 0);
}
// updates AABB while 9-slicing
_updateAabb(aabb) {
aabb.center.set(0, 0, 0);
aabb.halfExtents.set(this._outerScale.x * 0.5, this._outerScale.y * 0.5, 1e-3);
aabb.setFromTransformedAabb(aabb, this._node.getWorldTransform());
return aabb;
}
_tryAutoPlay() {
if (!this._autoPlayClip) return;
if (this.type !== SPRITETYPE_ANIMATED) return;
const clip = this._clips[this._autoPlayClip];
if (clip && !clip.isPlaying && (!this._currentClip || !this._currentClip.isPlaying)) {
if (this.enabled && this.entity.enabled) {
this.play(clip.name);
}
}
}
_onLayersChanged(oldComp, newComp) {
oldComp.off("add", this._onLayerAdded, this);
oldComp.off("remove", this._onLayerRemoved, this);
newComp.on("add", this._onLayerAdded, this);
newComp.on("remove", this._onLayerRemoved, this);
if (this.enabled && this.entity.enabled) {
this.addToLayers();
}
}
_onLayerAdded(layer) {
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
if (this._inLayers && this.enabled && this.entity.enabled && this._meshInstance) {
layer.addMeshInstances([this._meshInstance]);
}
}
_onLayerRemoved(layer) {
if (!this._meshInstance) return;
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.removeMeshInstances([this._meshInstance]);
}
addClip(data) {
const clip = new SpriteAnimationClip(this, {
name: data.name,
fps: data.fps,
loop: data.loop,
spriteAsset: data.spriteAsset
});
this._clips[data.name] = clip;
if (clip.name && clip.name === this._autoPlayClip) {
this._tryAutoPlay();
}
return clip;
}
removeClip(name) {
delete this._clips[name];
}
clip(name) {
return this._clips[name];
}
play(name) {
const clip = this._clips[name];
const current = this._currentClip;
if (current && current !== clip) {
current._playing = false;
}
this._currentClip = clip;
if (this._currentClip) {
this._currentClip = clip;
this._currentClip.play();
} else {
}
return clip;
}
pause() {
if (this._currentClip === this._defaultClip) return;
if (this._currentClip.isPlaying) {
this._currentClip.pause();
}
}
resume() {
if (this._currentClip === this._defaultClip) return;
if (this._currentClip.isPaused) {
this._currentClip.resume();
}
}
stop() {
if (this._currentClip === this._defaultClip) return;
this._currentClip.stop();
}
}
export {
SpriteComponent
};