playcanvas
Version:
PlayCanvas WebGL game engine
1,473 lines (1,472 loc) • 82.8 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { TRACEID_ELEMENT } from '../../../core/constants.js';
import { Mat4 } from '../../../core/math/mat4.js';
import { Vec2 } from '../../../core/math/vec2.js';
import { Vec3 } from '../../../core/math/vec3.js';
import { Vec4 } from '../../../core/math/vec4.js';
import { FUNC_EQUAL, STENCILOP_INCREMENT, STENCILOP_REPLACE, FUNC_ALWAYS } from '../../../platform/graphics/constants.js';
import { LAYERID_UI } from '../../../scene/constants.js';
import { BatchGroup } from '../../../scene/batching/batch-group.js';
import { StencilParameters } from '../../../platform/graphics/stencil-parameters.js';
import { Entity } from '../../entity.js';
import { Component } from '../component.js';
import { ELEMENTTYPE_GROUP, FITMODE_STRETCH, ELEMENTTYPE_IMAGE, ELEMENTTYPE_TEXT } from './constants.js';
import { ImageElement } from './image-element.js';
import { TextElement } from './text-element.js';
/**
* @import { BoundingBox } from '../../../core/shape/bounding-box.js'
* @import { CanvasFont } from '../../../framework/font/canvas-font.js'
* @import { Color } from '../../../core/math/color.js'
* @import { ElementComponentData } from './data.js'
* @import { ElementComponentSystem } from './system.js'
* @import { EventHandle } from '../../../core/event-handle.js'
* @import { Font } from '../../../framework/font/font.js'
* @import { Material } from '../../../scene/materials/material.js'
* @import { Sprite } from '../../../scene/sprite.js'
* @import { Texture } from '../../../platform/graphics/texture.js'
*/ const position = new Vec3();
const invParentWtm = new Mat4();
const vecA = new Vec3();
const vecB = new Vec3();
const matA = new Mat4();
const matB = new Mat4();
const matC = new Mat4();
const matD = new Mat4();
/**
* ElementComponents are used to construct user interfaces. The {@link ElementComponent#type}
* property can be configured in 3 main ways: as a text element, as an image element or as a group
* element. If the ElementComponent has a {@link ScreenComponent} ancestor in the hierarchy, it
* will be transformed with respect to the coordinate system of the screen. If there is no
* {@link ScreenComponent} ancestor, the ElementComponent will be transformed like any other
* entity.
*
* You should never need to use the ElementComponent constructor directly. To add an
* ElementComponent to an {@link Entity}, use {@link Entity#addComponent}:
*
* ```javascript
* const entity = pc.Entity();
* entity.addComponent('element'); // This defaults to a 'group' element
* ```
*
* To create a simple text-based element:
*
* ```javascript
* entity.addComponent('element', {
* anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5), // centered anchor
* fontAsset: fontAsset,
* fontSize: 128,
* pivot: new pc.Vec2(0.5, 0.5), // centered pivot
* text: 'Hello World!',
* type: pc.ELEMENTTYPE_TEXT
* });
* ```
*
* Once the ElementComponent is added to the entity, you can access it via the
* {@link Entity#element} property:
*
* ```javascript
* entity.element.color = pc.Color.RED; // Set the element's color to red
*
* console.log(entity.element.color); // Get the element's color and print it
* ```
*
* Relevant Engine API examples:
*
* - [Basic text rendering](https://playcanvas.github.io/#/user-interface/text)
* - [Auto font sizing](https://playcanvas.github.io/#/user-interface/text-auto-font-size)
* - [Emojis](https://playcanvas.github.io/#/user-interface/text-emojis)
* - [Text localization](https://playcanvas.github.io/#/user-interface/text-localization)
* - [Typewriter text](https://playcanvas.github.io/#/user-interface/text-typewriter)
*
* @hideconstructor
* @category User Interface
*/ class ElementComponent extends Component {
static{
/**
* Fired when the mouse is pressed while the cursor is on the component. Only fired when
* useInput is true. The handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mousedown', (event) => {
* console.log(`Mouse down event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSEDOWN = 'mousedown';
}
static{
/**
* Fired when the mouse is released while the cursor is on the component. Only fired when
* useInput is true. The handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mouseup', (event) => {
* console.log(`Mouse up event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSEUP = 'mouseup';
}
static{
/**
* Fired when the mouse cursor enters the component. Only fired when useInput is true. The
* handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mouseenter', (event) => {
* console.log(`Mouse enter event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSEENTER = 'mouseenter';
}
static{
/**
* Fired when the mouse cursor leaves the component. Only fired when useInput is true. The
* handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mouseleave', (event) => {
* console.log(`Mouse leave event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSELEAVE = 'mouseleave';
}
static{
/**
* Fired when the mouse cursor is moved on the component. Only fired when useInput is true. The
* handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mousemove', (event) => {
* console.log(`Mouse move event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSEMOVE = 'mousemove';
}
static{
/**
* Fired when the mouse wheel is scrolled on the component. Only fired when useInput is true.
* The handler is passed an {@link ElementMouseEvent}.
*
* @event
* @example
* entity.element.on('mousewheel', (event) => {
* console.log(`Mouse wheel event on entity ${entity.name}`);
* });
*/ this.EVENT_MOUSEWHEEL = 'mousewheel';
}
static{
/**
* Fired when the mouse is pressed and released on the component or when a touch starts and
* ends on the component. Only fired when useInput is true. The handler is passed an
* {@link ElementMouseEvent} or {@link ElementTouchEvent}.
*
* @event
* @example
* entity.element.on('click', (event) => {
* console.log(`Click event on entity ${entity.name}`);
* });
*/ this.EVENT_CLICK = 'click';
}
static{
/**
* Fired when a touch starts on the component. Only fired when useInput is true. The handler is
* passed an {@link ElementTouchEvent}.
*
* @event
* @example
* entity.element.on('touchstart', (event) => {
* console.log(`Touch start event on entity ${entity.name}`);
* });
*/ this.EVENT_TOUCHSTART = 'touchstart';
}
static{
/**
* Fired when a touch ends on the component. Only fired when useInput is true. The handler is
* passed an {@link ElementTouchEvent}.
*
* @event
* @example
* entity.element.on('touchend', (event) => {
* console.log(`Touch end event on entity ${entity.name}`);
* });
*/ this.EVENT_TOUCHEND = 'touchend';
}
static{
/**
* Fired when a touch moves after it started touching the component. Only fired when useInput
* is true. The handler is passed an {@link ElementTouchEvent}.
*
* @event
* @example
* entity.element.on('touchmove', (event) => {
* console.log(`Touch move event on entity ${entity.name}`);
* });
*/ this.EVENT_TOUCHMOVE = 'touchmove';
}
static{
/**
* Fired when a touch is canceled on the component. Only fired when useInput is true. The
* handler is passed an {@link ElementTouchEvent}.
*
* @event
* @example
* entity.element.on('touchcancel', (event) => {
* console.log(`Touch cancel event on entity ${entity.name}`);
* });
*/ this.EVENT_TOUCHCANCEL = 'touchcancel';
}
/**
* Create a new ElementComponent instance.
*
* @param {ElementComponentSystem} system - The ComponentSystem that created this Component.
* @param {Entity} entity - The Entity that this Component is attached to.
*/ constructor(system, entity){
super(system, entity), /**
* @type {EventHandle|null}
* @private
*/ this._evtLayersChanged = null, /**
* @type {EventHandle|null}
* @private
*/ this._evtLayerAdded = null, /**
* @type {EventHandle|null}
* @private
*/ this._evtLayerRemoved = null;
// set to true by the ElementComponentSystem while
// the component is being initialized
this._beingInitialized = false;
this._anchor = new Vec4();
this._localAnchor = new Vec4();
this._pivot = new Vec2();
this._width = this._calculatedWidth = 32;
this._height = this._calculatedHeight = 32;
this._margin = new Vec4(0, 0, -32, -32);
// the model transform used to render
this._modelTransform = new Mat4();
this._screenToWorld = new Mat4();
// transform that updates local position according to anchor values
this._anchorTransform = new Mat4();
this._anchorDirty = true;
// transforms to calculate screen coordinates
this._parentWorldTransform = new Mat4();
this._screenTransform = new Mat4();
// the corners of the element relative to its screen component.
// Order is bottom left, bottom right, top right, top left
this._screenCorners = [
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3()
];
// canvas-space corners of the element.
// Order is bottom left, bottom right, top right, top left
this._canvasCorners = [
new Vec2(),
new Vec2(),
new Vec2(),
new Vec2()
];
// the world space corners of the element
// Order is bottom left, bottom right, top right, top left
this._worldCorners = [
new Vec3(),
new Vec3(),
new Vec3(),
new Vec3()
];
this._cornersDirty = true;
this._canvasCornersDirty = true;
this._worldCornersDirty = true;
this.entity.on('insert', this._onInsert, this);
this._patch();
/**
* The Entity with a {@link ScreenComponent} that this component belongs to. This is
* automatically set when the component is a child of a ScreenComponent.
*
* @type {Entity|null}
*/ this.screen = null;
this._type = ELEMENTTYPE_GROUP;
// element types
this._image = null;
this._text = null;
this._group = null;
this._drawOrder = 0;
// Fit mode
this._fitMode = FITMODE_STRETCH;
// input related
this._useInput = false;
this._layers = [
LAYERID_UI
]; // assign to the default UI layer
this._addedModels = []; // store models that have been added to layer so we can re-add when layer is changed
this._batchGroupId = -1;
this._batchGroup = null;
//
this._offsetReadAt = 0;
this._maskOffset = 0.5;
this._maskedBy = null; // the entity that is masking this element
}
// TODO: Remove this override in upgrading component
/**
* @type {ElementComponentData}
* @ignore
*/ get data() {
const record = this.system.store[this.entity.getGuid()];
return record ? record.data : null;
}
/**
* Sets the enabled state of the component.
*
* @type {boolean}
*/ set enabled(value) {
const data = this.data;
const oldValue = data.enabled;
data.enabled = value;
this.fire('set', 'enabled', oldValue, value);
}
/**
* Gets the enabled state of the component.
*
* @type {boolean}
*/ get enabled() {
return this.data.enabled;
}
/**
* @type {number}
* @private
*/ get _absLeft() {
return this._localAnchor.x + this._margin.x;
}
/**
* @type {number}
* @private
*/ get _absRight() {
return this._localAnchor.z - this._margin.z;
}
/**
* @type {number}
* @private
*/ get _absTop() {
return this._localAnchor.w - this._margin.w;
}
/**
* @type {number}
* @private
*/ get _absBottom() {
return this._localAnchor.y + this._margin.y;
}
/**
* @type {boolean}
* @private
*/ get _hasSplitAnchorsX() {
return Math.abs(this._anchor.x - this._anchor.z) > 0.001;
}
/**
* @type {boolean}
* @private
*/ get _hasSplitAnchorsY() {
return Math.abs(this._anchor.y - this._anchor.w) > 0.001;
}
/**
* Gets the world space axis-aligned bounding box for this element component.
*
* @type {BoundingBox | null}
*/ get aabb() {
if (this._image) {
return this._image.aabb;
}
if (this._text) {
return this._text.aabb;
}
return null;
}
/**
* Sets the anchor for this element component. Specifies where the left, bottom, right and top
* edges of the component are anchored relative to its parent. Each value ranges from 0 to 1.
* e.g. a value of `[0, 0, 0, 0]` means that the element will be anchored to the bottom left of
* its parent. A value of `[1, 1, 1, 1]` means it will be anchored to the top right. A split
* anchor is when the left-right or top-bottom pairs of the anchor are not equal. In that case,
* the component will be resized to cover that entire area. For example, a value of `[0, 0, 1, 1]`
* will make the component resize exactly as its parent.
*
* @example
* this.entity.element.anchor = new pc.Vec4(Math.random() * 0.1, 0, 1, 0);
* @example
* this.entity.element.anchor = [Math.random() * 0.1, 0, 1, 0];
*
* @type {Vec4 | number[]}
*/ set anchor(value) {
if (value instanceof Vec4) {
this._anchor.copy(value);
} else {
this._anchor.set(...value);
}
if (!this.entity._parent && !this.screen) {
this._calculateLocalAnchors();
} else {
this._calculateSize(this._hasSplitAnchorsX, this._hasSplitAnchorsY);
}
this._anchorDirty = true;
if (!this.entity._dirtyLocal) {
this.entity._dirtifyLocal();
}
this.fire('set:anchor', this._anchor);
}
/**
* Gets the anchor for this element component.
*
* @type {Vec4 | number[]}
*/ get anchor() {
return this._anchor;
}
/**
* Sets the batch group (see {@link BatchGroup}) for this element. Default is -1 (no group).
*
* @type {number}
*/ set batchGroupId(value) {
if (this._batchGroupId === value) {
return;
}
if (this.entity.enabled && this._batchGroupId >= 0) {
this.system.app.batcher?.remove(BatchGroup.ELEMENT, this.batchGroupId, this.entity);
}
if (this.entity.enabled && value >= 0) {
this.system.app.batcher?.insert(BatchGroup.ELEMENT, value, this.entity);
}
if (value < 0 && this._batchGroupId >= 0 && this.enabled && this.entity.enabled) {
// re-add model to scene, in case it was removed by batching
if (this._image && this._image._renderable.model) {
this.addModelToLayers(this._image._renderable.model);
} else if (this._text && this._text._model) {
this.addModelToLayers(this._text._model);
}
}
this._batchGroupId = value;
}
/**
* Gets the batch group (see {@link BatchGroup}) for this element.
*
* @type {number}
*/ get batchGroupId() {
return this._batchGroupId;
}
/**
* Sets the distance from the bottom edge of the anchor. Can be used in combination with a
* split anchor to make the component's top edge always be 'top' units away from the top.
*
* @type {number}
*/ set bottom(value) {
this._margin.y = value;
const p = this.entity.getLocalPosition();
const wt = this._absTop;
const wb = this._localAnchor.y + value;
this._setHeight(wt - wb);
p.y = value + this._calculatedHeight * this._pivot.y;
this.entity.setLocalPosition(p);
}
/**
* Gets the distance from the bottom edge of the anchor.
*
* @type {number}
*/ get bottom() {
return this._margin.y;
}
/**
* Sets the width at which the element will be rendered. In most cases this will be the same as
* {@link width}. However, in some cases the engine may calculate a different width for the
* element, such as when the element is under the control of a {@link LayoutGroupComponent}. In
* these scenarios, `calculatedWidth` may be smaller or larger than the width that was set in
* the editor.
*
* @type {number}
*/ set calculatedWidth(value) {
this._setCalculatedWidth(value, true);
}
/**
* Gets the width at which the element will be rendered.
*
* @type {number}
*/ get calculatedWidth() {
return this._calculatedWidth;
}
/**
* Sets the height at which the element will be rendered. In most cases this will be the same
* as {@link height}. However, in some cases the engine may calculate a different height for
* the element, such as when the element is under the control of a {@link LayoutGroupComponent}.
* In these scenarios, `calculatedHeight` may be smaller or larger than the height that was set
* in the editor.
*
* @type {number}
*/ set calculatedHeight(value) {
this._setCalculatedHeight(value, true);
}
/**
* Gets the height at which the element will be rendered.
*
* @type {number}
*/ get calculatedHeight() {
return this._calculatedHeight;
}
/**
* Gets the array of 4 {@link Vec2}s that represent the bottom left, bottom right, top right
* and top left corners of the component in canvas pixels. Only works for screen space element
* components.
*
* @type {Vec2[]}
*/ get canvasCorners() {
if (!this._canvasCornersDirty || !this.screen || !this.screen.screen.screenSpace) {
return this._canvasCorners;
}
const device = this.system.app.graphicsDevice;
const screenCorners = this.screenCorners;
const sx = device.canvas.clientWidth / device.width;
const sy = device.canvas.clientHeight / device.height;
// scale screen corners to canvas size and reverse y
for(let i = 0; i < 4; i++){
this._canvasCorners[i].set(screenCorners[i].x * sx, (device.height - screenCorners[i].y) * sy);
}
this._canvasCornersDirty = false;
return this._canvasCorners;
}
/**
* Sets the draw order of the component. A higher value means that the component will be
* rendered on top of other components.
*
* @type {number}
*/ set drawOrder(value) {
let priority = 0;
if (this.screen) {
priority = this.screen.screen.priority;
}
if (value > 0xFFFFFF) {
Debug.warn(`Element.drawOrder larger than max size of: ${0xFFFFFF}`);
value = 0xFFFFFF;
}
// screen priority is stored in the top 8 bits
this._drawOrder = (priority << 24) + value;
this.fire('set:draworder', this._drawOrder);
}
/**
* Gets the draw order of the component.
*
* @type {number}
*/ get drawOrder() {
return this._drawOrder;
}
/**
* Sets the height of the element as set in the editor. Note that in some cases this may not
* reflect the true height at which the element is rendered, such as when the element is under
* the control of a {@link LayoutGroupComponent}. See {@link calculatedHeight} in order to
* ensure you are reading the true height at which the element will be rendered.
*
* @type {number}
*/ set height(value) {
this._height = value;
if (!this._hasSplitAnchorsY) {
this._setCalculatedHeight(value, true);
}
this.fire('set:height', this._height);
}
/**
* Gets the height of the element.
*
* @type {number}
*/ get height() {
return this._height;
}
/**
* Sets the array of layer IDs ({@link Layer#id}) to which this element should belong. Don't
* push, pop, splice or modify this array. If you want to change it, set a new one instead.
*
* @type {number[]}
*/ set layers(value) {
if (this._addedModels.length) {
for(let i = 0; i < this._layers.length; i++){
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
for(let j = 0; j < this._addedModels.length; j++){
layer.removeMeshInstances(this._addedModels[j].meshInstances);
}
}
}
}
this._layers = value;
if (!this.enabled || !this.entity.enabled || !this._addedModels.length) {
return;
}
for(let i = 0; i < this._layers.length; i++){
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
for(let j = 0; j < this._addedModels.length; j++){
layer.addMeshInstances(this._addedModels[j].meshInstances);
}
}
}
}
/**
* Gets the array of layer IDs ({@link Layer#id}) to which this element belongs.
*
* @type {number[]}
*/ get layers() {
return this._layers;
}
/**
* Sets the distance from the left edge of the anchor. Can be used in combination with a split
* anchor to make the component's left edge always be 'left' units away from the left.
*
* @type {number}
*/ set left(value) {
this._margin.x = value;
const p = this.entity.getLocalPosition();
const wr = this._absRight;
const wl = this._localAnchor.x + value;
this._setWidth(wr - wl);
p.x = value + this._calculatedWidth * this._pivot.x;
this.entity.setLocalPosition(p);
}
/**
* Gets the distance from the left edge of the anchor.
*
* @type {number}
*/ get left() {
return this._margin.x;
}
/**
* Sets the distance from the left, bottom, right and top edges of the anchor. For example, if
* we are using a split anchor like `[0, 0, 1, 1]` and the margin is `[0, 0, 0, 0]` then the
* component will be the same width and height as its parent.
*
* @type {Vec4}
*/ set margin(value) {
this._margin.copy(value);
this._calculateSize(true, true);
this.fire('set:margin', this._margin);
}
/**
* Gets the distance from the left, bottom, right and top edges of the anchor.
*
* @type {Vec4}
*/ get margin() {
return this._margin;
}
/**
* Gets the entity that is currently masking this element.
*
* @type {Entity}
* @private
*/ get maskedBy() {
return this._maskedBy;
}
/**
* Sets the position of the pivot of the component relative to its anchor. Each value ranges
* from 0 to 1 where `[0, 0]` is the bottom left and `[1, 1]` is the top right.
*
* @example
* this.entity.element.pivot = [Math.random() * 0.1, Math.random() * 0.1];
* @example
* this.entity.element.pivot = new pc.Vec2(Math.random() * 0.1, Math.random() * 0.1);
*
* @type {Vec2 | number[]}
*/ set pivot(value) {
const { pivot, margin } = this;
const prevX = pivot.x;
const prevY = pivot.y;
if (value instanceof Vec2) {
pivot.copy(value);
} else {
pivot.set(...value);
}
const mx = margin.x + margin.z;
const dx = pivot.x - prevX;
margin.x += mx * dx;
margin.z -= mx * dx;
const my = margin.y + margin.w;
const dy = pivot.y - prevY;
margin.y += my * dy;
margin.w -= my * dy;
this._anchorDirty = true;
this._cornersDirty = true;
this._worldCornersDirty = true;
this._calculateSize(false, false);
// we need to flag children as dirty too
// in order for them to update their position
this._flagChildrenAsDirty();
this.fire('set:pivot', pivot);
}
/**
* Gets the position of the pivot of the component relative to its anchor.
*
* @type {Vec2 | number[]}
*/ get pivot() {
return this._pivot;
}
/**
* Sets the distance from the right edge of the anchor. Can be used in combination with a split
* anchor to make the component's right edge always be 'right' units away from the right.
*
* @type {number}
*/ set right(value) {
this._margin.z = value;
// update width
const p = this.entity.getLocalPosition();
const wl = this._absLeft;
const wr = this._localAnchor.z - value;
this._setWidth(wr - wl);
// update position
p.x = this._localAnchor.z - this._localAnchor.x - value - this._calculatedWidth * (1 - this._pivot.x);
this.entity.setLocalPosition(p);
}
/**
* Gets the distance from the right edge of the anchor.
*
* @type {number}
*/ get right() {
return this._margin.z;
}
/**
* Gets the array of 4 {@link Vec3}s that represent the bottom left, bottom right, top right
* and top left corners of the component relative to its parent {@link ScreenComponent}.
*
* @type {Vec3[]}
*/ get screenCorners() {
if (!this._cornersDirty || !this.screen) {
return this._screenCorners;
}
const parentBottomLeft = this.entity.parent && this.entity.parent.element && this.entity.parent.element.screenCorners[0];
// init corners
this._screenCorners[0].set(this._absLeft, this._absBottom, 0);
this._screenCorners[1].set(this._absRight, this._absBottom, 0);
this._screenCorners[2].set(this._absRight, this._absTop, 0);
this._screenCorners[3].set(this._absLeft, this._absTop, 0);
// transform corners to screen space
const screenSpace = this.screen.screen.screenSpace;
for(let i = 0; i < 4; i++){
this._screenTransform.transformPoint(this._screenCorners[i], this._screenCorners[i]);
if (screenSpace) {
this._screenCorners[i].mulScalar(this.screen.screen.scale);
}
if (parentBottomLeft) {
this._screenCorners[i].add(parentBottomLeft);
}
}
this._cornersDirty = false;
this._canvasCornersDirty = true;
this._worldCornersDirty = true;
return this._screenCorners;
}
/**
* Gets the width of the text rendered by the component. Only works for
* {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/ get textWidth() {
return this._text ? this._text.width : 0;
}
/**
* Gets the height of the text rendered by the component. Only works for
* {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/ get textHeight() {
return this._text ? this._text.height : 0;
}
/**
* Sets the distance from the top edge of the anchor. Can be used in combination with a split
* anchor to make the component's bottom edge always be 'bottom' units away from the bottom.
*
* @type {number}
*/ set top(value) {
this._margin.w = value;
const p = this.entity.getLocalPosition();
const wb = this._absBottom;
const wt = this._localAnchor.w - value;
this._setHeight(wt - wb);
p.y = this._localAnchor.w - this._localAnchor.y - value - this._calculatedHeight * (1 - this._pivot.y);
this.entity.setLocalPosition(p);
}
/**
* Gets the distance from the top edge of the anchor.
*
* @type {number}
*/ get top() {
return this._margin.w;
}
/**
* Sets the type of the ElementComponent. Can be:
*
* - {@link ELEMENTTYPE_GROUP}: The component can be used as a layout mechanism to create
* groups of ElementComponents e.g. panels.
* - {@link ELEMENTTYPE_IMAGE}: The component will render an image
* - {@link ELEMENTTYPE_TEXT}: The component will render text
*
* @type {string}
*/ set type(value) {
if (value !== this._type) {
this._type = value;
if (this._image) {
this._image.destroy();
this._image = null;
}
if (this._text) {
this._text.destroy();
this._text = null;
}
if (value === ELEMENTTYPE_IMAGE) {
this._image = new ImageElement(this);
} else if (value === ELEMENTTYPE_TEXT) {
this._text = new TextElement(this);
}
}
}
/**
* Gets the type of the ElementComponent.
*
* @type {string}
*/ get type() {
return this._type;
}
/**
* Sets whether the component will receive mouse and touch input events.
*
* @type {boolean}
*/ set useInput(value) {
if (this._useInput === value) {
return;
}
this._useInput = value;
if (this.system.app.elementInput) {
if (value) {
if (this.enabled && this.entity.enabled) {
this.system.app.elementInput.addElement(this);
}
} else {
this.system.app.elementInput.removeElement(this);
}
} else {
if (this._useInput === true) {
Debug.warn('Elements will not get any input events because this.system.app.elementInput is not created');
}
}
this.fire('set:useInput', value);
}
/**
* Gets whether the component will receive mouse and touch input events.
*
* @type {boolean}
*/ get useInput() {
return this._useInput;
}
/**
* Sets the fit mode of the element. Controls how the content should be fitted and preserve the
* aspect ratio of the source texture or sprite. Only works for {@link ELEMENTTYPE_IMAGE}
* types. Can be:
*
* - {@link FITMODE_STRETCH}: Fit the content exactly to Element's bounding box.
* - {@link FITMODE_CONTAIN}: Fit the content within the Element's bounding box while
* preserving its Aspect Ratio.
* - {@link FITMODE_COVER}: Fit the content to cover the entire Element's bounding box while
* preserving its Aspect Ratio.
*
* @type {string}
*/ set fitMode(value) {
this._fitMode = value;
this._calculateSize(true, true);
if (this._image) {
this._image.refreshMesh();
}
}
/**
* Gets the fit mode of the element.
*
* @type {string}
*/ get fitMode() {
return this._fitMode;
}
/**
* Sets the width of the element as set in the editor. Note that in some cases this may not
* reflect the true width at which the element is rendered, such as when the element is under
* the control of a {@link LayoutGroupComponent}. See {@link calculatedWidth} in order to
* ensure you are reading the true width at which the element will be rendered.
*
* @type {number}
*/ set width(value) {
this._width = value;
if (!this._hasSplitAnchorsX) {
this._setCalculatedWidth(value, true);
}
this.fire('set:width', this._width);
}
/**
* Gets the width of the element.
*
* @type {number}
*/ get width() {
return this._width;
}
/**
* Gets the array of 4 {@link Vec3}s that represent the bottom left, bottom right, top right
* and top left corners of the component in world space. Only works for 3D element components.
*
* @type {Vec3[]}
*/ get worldCorners() {
if (!this._worldCornersDirty) {
return this._worldCorners;
}
if (this.screen) {
const screenCorners = this.screenCorners;
if (!this.screen.screen.screenSpace) {
matA.copy(this.screen.screen._screenMatrix);
// flip screen matrix along the horizontal axis
matA.data[13] = -matA.data[13];
// create transform that brings screen corners to world space
matA.mul2(this.screen.getWorldTransform(), matA);
// transform screen corners to world space
for(let i = 0; i < 4; i++){
matA.transformPoint(screenCorners[i], this._worldCorners[i]);
}
}
} else {
const localPos = this.entity.getLocalPosition();
// rotate and scale around pivot
matA.setTranslate(-localPos.x, -localPos.y, -localPos.z);
matB.setTRS(Vec3.ZERO, this.entity.getLocalRotation(), this.entity.getLocalScale());
matC.setTranslate(localPos.x, localPos.y, localPos.z);
// get parent world transform (but use this entity if there is no parent)
const entity = this.entity.parent ? this.entity.parent : this.entity;
matD.copy(entity.getWorldTransform());
matD.mul(matC).mul(matB).mul(matA);
// bottom left
vecA.set(localPos.x - this.pivot.x * this.calculatedWidth, localPos.y - this.pivot.y * this.calculatedHeight, localPos.z);
matD.transformPoint(vecA, this._worldCorners[0]);
// bottom right
vecA.set(localPos.x + (1 - this.pivot.x) * this.calculatedWidth, localPos.y - this.pivot.y * this.calculatedHeight, localPos.z);
matD.transformPoint(vecA, this._worldCorners[1]);
// top right
vecA.set(localPos.x + (1 - this.pivot.x) * this.calculatedWidth, localPos.y + (1 - this.pivot.y) * this.calculatedHeight, localPos.z);
matD.transformPoint(vecA, this._worldCorners[2]);
// top left
vecA.set(localPos.x - this.pivot.x * this.calculatedWidth, localPos.y + (1 - this.pivot.y) * this.calculatedHeight, localPos.z);
matD.transformPoint(vecA, this._worldCorners[3]);
}
this._worldCornersDirty = false;
return this._worldCorners;
}
/**
* Sets the size of the font. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/ set fontSize(arg) {
this._setValue('fontSize', arg);
}
/**
* Gets the size of the font.
*
* @type {number}
*/ get fontSize() {
if (this._text) {
return this._text.fontSize;
}
return null;
}
/**
* Sets the minimum size that the font can scale to when {@link autoFitWidth} or
* {@link autoFitHeight} are true.
*
* @type {number}
*/ set minFontSize(arg) {
this._setValue('minFontSize', arg);
}
/**
* Gets the minimum size that the font can scale to when {@link autoFitWidth} or
* {@link autoFitHeight} are true.
*
* @type {number}
*/ get minFontSize() {
if (this._text) {
return this._text.minFontSize;
}
return null;
}
/**
* Sets the maximum size that the font can scale to when {@link autoFitWidth} or
* {@link autoFitHeight} are true.
*
* @type {number}
*/ set maxFontSize(arg) {
this._setValue('maxFontSize', arg);
}
/**
* Gets the maximum size that the font can scale to when {@link autoFitWidth} or
* {@link autoFitHeight} are true.
*
* @type {number}
*/ get maxFontSize() {
if (this._text) {
return this._text.maxFontSize;
}
return null;
}
/**
* Sets the maximum number of lines that the Element can wrap to. Any leftover text will be
* appended to the last line. Set this to null to allow unlimited lines.
*
* @type {number|null}
*/ set maxLines(arg) {
this._setValue('maxLines', arg);
}
/**
* Gets the maximum number of lines that the Element can wrap to. Returns null for unlimited
* lines.
*
* @type {number|null}
*/ get maxLines() {
if (this._text) {
return this._text.maxLines;
}
return null;
}
/**
* Sets whether the font size and line height will scale so that the text fits inside the width
* of the Element. The font size will be scaled between {@link minFontSize} and
* {@link maxFontSize}. The value of {@link autoFitWidth} will be ignored if {@link autoWidth}
* is true.
*
* @type {boolean}
*/ set autoFitWidth(arg) {
this._setValue('autoFitWidth', arg);
}
/**
* Gets whether the font size and line height will scale so that the text fits inside the width
* of the Element.
*
* @type {boolean}
*/ get autoFitWidth() {
if (this._text) {
return this._text.autoFitWidth;
}
return null;
}
/**
* Sets whether the font size and line height will scale so that the text fits inside the
* height of the Element. The font size will be scaled between {@link minFontSize} and
* {@link maxFontSize}. The value of {@link autoFitHeight} will be ignored if
* {@link autoHeight} is true.
*
* @type {boolean}
*/ set autoFitHeight(arg) {
this._setValue('autoFitHeight', arg);
}
/**
* Gets whether the font size and line height will scale so that the text fits inside the
* height of the Element.
*
* @type {boolean}
*/ get autoFitHeight() {
if (this._text) {
return this._text.autoFitHeight;
}
return null;
}
/**
* Sets the color of the image for {@link ELEMENTTYPE_IMAGE} types or the color of the text for
* {@link ELEMENTTYPE_TEXT} types.
*
* @type {Color}
*/ set color(arg) {
this._setValue('color', arg);
}
/**
* Gets the color of the element.
*
* @type {Color}
*/ get color() {
if (this._text) {
return this._text.color;
}
if (this._image) {
return this._image.color;
}
return null;
}
/**
* Sets the font used for rendering the text. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {Font|CanvasFont}
*/ set font(arg) {
this._setValue('font', arg);
}
/**
* Gets the font used for rendering the text.
*
* @type {Font|CanvasFont}
*/ get font() {
if (this._text) {
return this._text.font;
}
return null;
}
/**
* Sets the id of the font asset used for rendering the text. Only works for {@link ELEMENTTYPE_TEXT}
* types.
*
* @type {number}
*/ set fontAsset(arg) {
this._setValue('fontAsset', arg);
}
/**
* Gets the id of the font asset used for rendering the text.
*
* @type {number}
*/ get fontAsset() {
if (this._text && typeof this._text.fontAsset === 'number') {
return this._text.fontAsset;
}
return null;
}
/**
* Sets the spacing between the letters of the text. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/ set spacing(arg) {
this._setValue('spacing', arg);
}
/**
* Gets the spacing between the letters of the text.
*
* @type {number}
*/ get spacing() {
if (this._text) {
return this._text.spacing;
}
return null;
}
/**
* Sets the height of each line of text. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/ set lineHeight(arg) {
this._setValue('lineHeight', arg);
}
/**
* Gets the height of each line of text.
*
* @type {number}
*/ get lineHeight() {
if (this._text) {
return this._text.lineHeight;
}
return null;
}
/**
* Sets whether to automatically wrap lines based on the element width. Only works for
* {@link ELEMENTTYPE_TEXT} types, and when {@link autoWidth} is set to false.
*
* @type {boolean}
*/ set wrapLines(arg) {
this._setValue('wrapLines', arg);
}
/**
* Gets whether to automatically wrap lines based on the element width.
*
* @type {boolean}
*/ get wrapLines() {
if (this._text) {
return this._text.wrapLines;
}
return null;
}
set lines(arg) {
this._setValue('lines', arg);
}
get lines() {
if (this._text) {
return this._text.lines;
}
return null;
}
/**
* Sets the horizontal and vertical alignment of the text. Values range from 0 to 1 where
* `[0, 0]` is the bottom left and `[1, 1]` is the top right. Only works for
* {@link ELEMENTTYPE_TEXT} types.
*
* @type {Vec2}
*/ set alignment(arg) {
this._setValue('alignment', arg);
}
/**
* Gets the horizontal and vertical alignment of the text.
*
* @type {Vec2}
*/ get alignment() {
if (this._text) {
return this._text.alignment;
}
return null;
}
/**
* Sets whether to automatically set the width of the component to be the same as the
* {@link textWidth}. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {boolean}
*/ set autoWidth(arg) {
this._setValue('autoWidth', arg);
}
/**
* Gets whether to automatically set the width of the component to be the same as the
* {@link textWidth}.
*
* @type {boolean}
*/ get autoWidth() {
if (this._text) {
return this._text.autoWidth;
}
return null;
}
/**
* Sets whether to automatically set the height of the component to be the same as the
* {@link textHeight}. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {boolean}
*/ set autoHeight(arg) {
this._setValue('autoHeight', arg);
}
/**
* Gets whether to automatically set the height of the component to be the same as the
* {@link textHeight}.
*
* @type {boolean}
*/ get autoHeight() {
if (this._text) {
return this._text.autoHeight;
}
return null;
}
/**
* Sets whether to reorder the text for RTL languages. The reordering uses a function
* registered by `app.systems.element.registerUnicodeConverter`.
*
* @type {boolean}
*/ set rtlReorder(arg) {
this._setValue('rtlReorder', arg);
}
/**
* Gets whether to reorder the text for RTL languages.
*
* @type {boolean}
*/ get rtlReorder() {
if (this._text) {
return this._text.rtlReorder;
}
return null;
}
/**
* Sets whether to convert unicode characters. This uses a function registered by
* `app.systems.element.registerUnicodeConverter`.
*
* @type {boolean}
*/ set unicodeConverter(arg) {
this._setValue('unicodeConverter', arg);
}
/**
* Gets whether to convert unicode characters.
*
* @type {boolean}
*/ get unicodeConverter() {
if (this._text) {
return this._text.unicodeConverter;
}
return null;
}
/**
* Sets the text to render. Only works for {@link ELEMENTTYPE_TEXT} types. To override certain
* text styling properties on a per-character basis, the text can optionally include markup
* tags contained within square brackets. Supported tags are:
*
* 1. `color` - override the element's {@link color} property. Examples:
* - `[color="#ff0000"]red text[/color]`
* - `[color="#00ff00"]green text[/color]`
* - `[color="#0000ff"]blue text[/color]`
* 2. `outline` - override the element's {@link outlineColor} and {@link outlineThickness}
* properties. Example:
* - `[outline color="#ffffff" thickness="0.5"]text[/outline]`
* 3. `shadow` - override the element's {@link shadowColor} and {@link shadowOffset}
* properties. Examples:
* - `[shadow color="#ffffff" offset="0.5"]text[/shadow]`
* - `[shadow color="#000000" offsetX="0.1" offsetY="0.2"]text[/shadow]`
*
* Note that markup tags are only processed if the text element's {@link enableMarkup} property
* is set to true.
*
* @type {string}
*/ set text(arg) {
this._setValue('text', arg);
}
/**
* Gets the text to render.
*
* @type {string}
*/ get text() {
if (this._text) {
return this._text.text;
}
return null;
}
/**
* Sets the localization key to use to get the localized text from {@link Application#i18n}.
* Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {string}
*/ set key(arg) {
this._setValue('key', arg);
}
/**
* Gets the localization key to use to get the localized text from {@link Application#i18n}.
*
* @type {string}
*/ get key() {
if (this._text) {
return this._text.key;
}
return null;
}
/**
* Sets the texture to render. Only works for {@link ELEMENTTYPE_IMAGE} types.
*
* @type {Texture}
*/ set texture(arg) {
this._setValue('texture', arg);
}
/**
* Gets the texture to render.
*
* @type {Texture}
*/ get texture() {
if (this._image) {
return this._image.texture;
}
return null;
}
/**
* Sets the id of the texture asset to render. Only works for {@link ELEMENTTYPE_IMAGE} types.
*
* @type {number}
*/ set textureAsset(arg) {
this._setValue('textureAsset', arg);
}
/**
* Gets the id of the texture asset to render.
*
* @type {number}
*/ get textureAsset() {
if (this._image) {
return this._image.textureAsset;
}
return null;
}
/**
* Sets the material to use when rendering an image. Only works for {@link ELEMENTTYPE_IMAGE} types.
*
* @type {Material}
*/ set material(arg) {
this._setValue('material', arg);
}
/**
* Gets the material to use when rendering an image.
*
* @type {Material}
*/ get material() {
if (this._image) {
return this._image.material;
}
return null;
}
/**
* Sets the id of the material asset to use when rendering an image. Only works for
* {@link ELEMENTTYPE_IMAGE} types.
*
* @type {number}
*/ set materialAsset(arg) {
this._setValue('materialAsset', arg);
}
/**
* Gets the id of the material asset to use when rendering an image.
*
* @type {number}
*/ get materialAsset() {
if (this._image) {
return this._image.materialAsset;
}
return null;
}
/**
* Sets the sprite to render. Only works for {@link ELEMENTTYPE_IMAGE} types which can render
* either a texture or a sprite.
*
* @type {Sprite}
*/ set sprite(arg) {
this._setValue('sprite', arg);
}
/**
* Gets the sprite to render.
*
* @type {Sprite}
*/ get sprite() {
if (this._image) {
return this._image.sprite;
}
return null;
}
/**
* Sets the id of the sprite asset to render. Only works for {@link ELEMENTTYPE_IMAGE} types which
* can render either a texture or a sprite.
*
* @type {number}
*/ set spriteAsset(arg) {
this._setValue('spriteAsset', arg);
}
/**
* Gets the id of the sprite asset to render.
*
* @type {number}
*/ get spriteAsset() {
if (this._image) {
return this._image.spriteAsset;
}
return null;
}
/**
* Sets the frame of the sprite to render. Only works for {@link ELEMENTTYPE_IMAGE} types who have a
* sprite assigned.
*
* @type {number}
*/ set spriteFrame(arg) {
this._setValue('spriteFrame', arg);
}
/**
* Gets the frame of the sprite to render.
*
* @type {number}
*/ get spriteFrame() {
if (this._image) {
return this._image.spriteFrame;
}
return null;
}
/**
* Sets the number of pixels that map to one PlayCanvas unit. Only works for
* {@link ELEMENTTYPE_IMAGE} types who have a sliced sprite assigned.
*
* @type {number}
*/ set pixelsPerUnit(arg) {
this._setValue('pixelsPerUnit', arg);
}
/**
* Gets the number of pixels that map to one PlayCanvas unit.
*
* @type {number}
*/ get pixelsPerUnit() {
if (this._image) {
return this._image.pixelsPerUnit;
}
return null;
}
/**
* Sets the opacity o