src/components/UiSprite.js
import VerticesRenderer from './VerticesRenderer';
import System from '../systems/System';
import { vec2, vec4 } from '../utils';
const cachedTemp1 = vec2.create();
const cachedTemp2 = vec2.create();
const cachedTemp3 = vec2.create();
export default class UiSprite extends VerticesRenderer {
static factory() {
return new UiSprite();
}
static get propsTypes() {
return {
visible: VerticesRenderer.propsTypes.visible,
shader: VerticesRenderer.propsTypes.shader,
overrideUniforms: VerticesRenderer.propsTypes.overrideUniforms,
overrideSamplers: VerticesRenderer.propsTypes.overrideSamplers,
layers: VerticesRenderer.propsTypes.layers,
overrideBaseTexture: 'string_null',
overrideBaseFiltering: 'enum(nearest, linear)',
camera: 'string_null',
width: 'number',
height: 'number',
widthAnchor: 'number',
heightAnchor: 'number',
xOrigin: 'number',
yOrigin: 'number',
topOffset: 'number',
bottomOffset: 'number',
leftOffset: 'number',
rightOffset: 'number',
leftOffset: 'number',
topBorder: 'number',
bottomBorder: 'number',
leftBorder: 'number',
rightBorder: 'number',
atlas: 'asset(atlas?:.*$)',
color: 'rgba'
};
}
get overrideBaseTexture() {
const { overrideSamplers } = this;
const sampler = overrideSamplers.sBase;
return !!sampler
? sampler.texture
: null;
}
set overrideBaseTexture(value) {
const { overrideSamplers } = this;
if (!value) {
delete overrideSamplers.sBase;
this.overrideSamplers = overrideSamplers;
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
const sampler = overrideSamplers['sBase'];
if (!sampler) {
overrideSamplers.sBase = {
texture: value,
filtering: 'linear'
};
} else {
sampler.texture = value;
}
this.overrideSamplers = overrideSamplers;
this._atlas = null;
this._frame = null;
}
get overrideBaseFiltering() {
const { overrideSamplers } = this;
const sampler = overrideSamplers.get('sBase');
return !!sampler
? sampler.filtering
: null;
}
set overrideBaseFiltering(value) {
const { overrideSamplers } = this;
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
const sampler = overrideSamplers.sBase;
if (!sampler) {
overrideSamplers.sBase = {
texture: '',
filtering: value
};
} else {
sampler.filtering = value;
}
this.overrideSamplers = overrideSamplers;
}
get camera() {
return this._camera;
}
set camera(value) {
if (!value) {
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
const { entity } = this;
this._camera = value;
if (!!entity) {
this._cameraEntity = entity.findEntity(value);
if (!this._cameraEntity) {
throw new Error(`Cannot find entity: ${value}`);
}
this._cameraComponent = this._cameraEntity.getComponent('Camera2D');
if (!this._cameraComponent) {
throw new Error(
`Entity doe not have Camera2D component: ${this._cameraEntity.path}`
);
}
}
this.recalculateSize();
}
get width() {
return this._width;
}
set width(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._width = value;
this.recalculateSize();
}
get height() {
return this._height;
}
set height(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._height = value;
this.recalculateSize();
}
get widthAnchor() {
return this._widthAnchor;
}
set widthAnchor(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._widthAnchor = value;
this.recalculateSize();
}
get heightAnchor() {
return this._heightAnchor;
}
set heightAnchor(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._heightAnchor = value;
this.recalculateSize();
}
get xOrigin() {
return this._xOrigin;
}
set xOrigin(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._xOrigin = value;
this.recalculateSize();
}
get yOrigin() {
return this._yOrigin;
}
set yOrigin(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._yOrigin = value;
this.recalculateSize();
}
get topOffset() {
return this._topOffset;
}
set topOffset(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._topOffset = value;
this._rebuild = true;
}
get bottomOffset() {
return this._bottomOffset;
}
set bottomOffset(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._bottomOffset = value;
this._rebuild = true;
}
get leftOffset() {
return this._leftOffset;
}
set leftOffset(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._leftOffset = value;
this._rebuild = true;
}
get rightOffset() {
return this._rightOffset;
}
set rightOffset(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._rightOffset = value;
this._rebuild = true;
}
get topBorder() {
return this._topBorder;
}
set topBorder(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._topBorder = Math.max(value, 0);
this._rebuild = true;
}
get bottomBorder() {
return this._bottomBorder;
}
set bottomBorder(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._bottomBorder = Math.max(value, 0);
this._rebuild = true;
}
get leftBorder() {
return this._leftBorder;
}
set leftBorder(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._leftBorder = Math.max(value, 0);
this._rebuild = true;
}
get rightBorder() {
return this._rightBorder;
}
set rightBorder(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._rightBorder = Math.max(value, 0);
this._rebuild = true;
}
get atlas() {
return this._atlas;
}
set atlas(value) {
if (!value || value === '') {
this._atlas = value;
this.overrideBaseTexture = '';
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
const found = value.indexOf(':');
if (found < 0) {
throw new Error('`value` does not conform rule of "atlas:frame" naming!');
}
const original = value;
const frame = value.substr(found + 1);
value = value.substr(0, found);
const assets = System.get('AssetSystem');
if (!assets) {
throw new Error('There is no registered AssetSystem!');
}
const atlas = assets.get(`atlas://${value}`);
if (!atlas) {
throw new Error(`There is no atlas asset loaded: ${value}`);
}
const { meta, frames } = atlas.data.descriptor;
if (!meta || !frames) {
throw new Error(`There is either no metadata or frames in atlas: ${value}`);
}
const info = frames[frame];
if (!info || !info.frame) {
throw new Error(`There is no frame information in atlas: ${value} (${frame})`);
}
this.overrideBaseTexture = meta.image;
this._atlas = original;
this._frame = info.frame;
this._rebuild = true;
}
get color() {
return this._color;
}
set color(value) {
if (typeof value === 'string') {
value = stringToRGBA(value);
}
if (!(value instanceof Array) && !(value instanceof Float32Array)) {
throw new Error('`value` is not type of either Array or Float32Array!');
}
if (value.length < 4) {
throw new Error('`value` array must have at least 4 items!');
}
vec4.copy(this._color, value);
const { overrideUniforms } = this;
overrideUniforms.uColor = this._color;
this.overrideUniforms = overrideUniforms;
}
get cachedWidth() {
return this._cachedWidth;
}
get cachedHeight() {
return this._cachedHeight;
}
constructor() {
super();
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
this._width = 0;
this._height = 0;
this._widthAnchor = 1;
this._heightAnchor = 1;
this._xOrigin = 0;
this._yOrigin = 0;
this._topOffset = 0;
this._bottomOffset = 0;
this._leftOffset = 0;
this._rightOffset = 0;
this._topBorder = 0;
this._bottomBorder = 0;
this._leftBorder = 0;
this._rightBorder = 0;
this._atlas = null;
this._color = vec4.fromValues(1, 1, 1, 1);
this._frame = null;
this._cachedWidth = 0;
this._cachedHeight = 0;
this._rebuild = true;
}
dispose() {
super.dispose();
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
this._atlas = null;
this._frame = null;
}
recalculateSize() {
this._cachedWidth = 0;
this._cachedHeight = 0;
const { entity, _width, _height, _widthAnchor, _heightAnchor } = this;
if (!entity) {
return;
}
const { parent } = entity;
if (!parent) {
return;
}
const sprite = parent.getComponent('UiSprite');
let pw = 0;
let ph = 0;
if (!!sprite) {
pw = sprite.cachedWidth;
ph = sprite.cachedHeight;
} else {
const { _cameraComponent } = this;
if (!!_cameraComponent) {
vec2.set(cachedTemp1, -1, -1);
vec2.transformMat4(
cachedTemp2,
cachedTemp1,
_cameraComponent.inverseProjectionMatrix
);
vec2.set(cachedTemp1, 1, 1);
vec2.transformMat4(
cachedTemp3,
cachedTemp1,
_cameraComponent.inverseProjectionMatrix
);
pw = Math.abs(cachedTemp3[0] - cachedTemp2[0]);
ph = Math.abs(cachedTemp3[1] - cachedTemp2[1]);
}
}
this._cachedWidth = (_width < 0 ? pw : _width) * _widthAnchor;
this._cachedHeight = (_height < 0 ? ph : _height) * _heightAnchor;
this._rebuild = true;
}
ensureVertices(renderer) {
if (!this._rebuild) {
return;
}
const meta = renderer.getTextureMeta(this.overrideBaseTexture) || {
width: 1,
height: 1
};
const {
_cachedWidth,
_cachedHeight,
_xOrigin,
_yOrigin,
_topOffset,
_bottomOffset,
_leftOffset,
_rightOffset,
_topBorder,
_bottomBorder,
_leftBorder,
_rightBorder,
_frame
} = this;
const ox = _cachedWidth * _xOrigin;
const oy = _cachedHeight * _yOrigin;
const p0c = _leftOffset - ox;
const p1c = p0c + _leftBorder;
const p3c = p0c + _cachedWidth - _rightOffset - _leftOffset;
const p2c = p3c - _rightBorder;
const p0r = _topOffset - oy;
const p1r = p0r + _topBorder;
const p3r = p0r + _cachedHeight - _bottomOffset - _topOffset;
const p2r = p3r - _bottomBorder;
if (!_frame) {
const t0c = 0;
const t1c = _leftBorder / meta.width;
const t2c = 1 - _rightBorder / meta.width;
const t3c = 1;
const t0r = 0;
const t1r = _topBorder / meta.width;
const t2r = 1 - _bottomBorder / meta.width;
const t3r = 1;
this.vertices = [
p0c, p0r, t0c, t0r,
p1c, p0r, t1c, t0r,
p2c, p0r, t2c, t0r,
p3c, p0r, t3c, t0r,
p0c, p1r, t0c, t1r,
p1c, p1r, t1c, t1r,
p2c, p1r, t2c, t1r,
p3c, p1r, t3c, t1r,
p0c, p2r, t0c, t2r,
p1c, p2r, t1c, t2r,
p2c, p2r, t2c, t2r,
p3c, p2r, t3c, t2r,
p0c, p3r, t0c, t3r,
p1c, p3r, t1c, t3r,
p2c, p3r, t2c, t3r,
p3c, p3r, t3c, t3r
];
} else {
const { x, y, w, h } = _frame;
const t0c = x / meta.width;
const t1c = t0c + _leftBorder / meta.width;
const t3c = (x + w) / meta.width;
const t2c = t3c - _rightBorder / meta.width;
const t0r = y / meta.height;
const t1r = t0r + _topBorder / meta.height;
const t3r = (y + h) / meta.height;
const t2r = t3r - _bottomBorder / meta.height;
this.vertices = [
p0c, p0r, t0c, t0r,
p1c, p0r, t1c, t0r,
p2c, p0r, t2c, t0r,
p3c, p0r, t3c, t0r,
p0c, p1r, t0c, t1r,
p1c, p1r, t1c, t1r,
p2c, p1r, t2c, t1r,
p3c, p1r, t3c, t1r,
p0c, p2r, t0c, t2r,
p1c, p2r, t1c, t2r,
p2c, p2r, t2c, t2r,
p3c, p2r, t3c, t2r,
p0c, p3r, t0c, t3r,
p1c, p3r, t1c, t3r,
p2c, p3r, t2c, t3r,
p3c, p3r, t3c, t3r
];
}
this.indices = [
0, 1, 5, 5, 4, 0,
1, 2, 6, 6, 5, 1,
2, 3, 7, 7, 6, 2,
4, 5, 9, 9, 8, 4,
5, 6,10,10, 9, 5,
6, 7,11,11,10, 6,
8, 9,13,13,12, 8,
9,10,14,14,13, 9,
10,11,15,15,14,10
];
this._rebuild = false;
// 0, 1, 2, 3,
// 4, 5, 6, 7,
// 8, 9,10,11,
//12,13,14,15
}
onAttach() {
super.onAttach();
const { overrideUniforms } = this;
overrideUniforms.uColor = this._color;
this.overrideUniforms = overrideUniforms;
this.camera = this.camera;
}
onAction(name, ...args) {
if (name === 'camera-changed') {
return this.onCameraChanged(...args);
} else {
return super.onAction(name, ...args);
}
}
onPropertySerialize(name, value) {
if (name === 'overrideSamplers') {
const result = super.onPropertySerialize(name, value);
if (!result) {
return null;
}
delete result.sBase;
return Object.keys(result).length > 0 ? result : null;
} else {
return super.onPropertySerialize(name, value);
}
}
onRender(gl, renderer, deltaTime, layer) {
this.ensureVertices(renderer);
super.onRender(gl, renderer, deltaTime, layer);
}
onCameraChanged(camera) {
if (camera === this._cameraComponent
&& (this._width < 0 || this._height < 0)
) {
this.recalculateSize();
}
}
}