playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,752 lines • 68.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
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_ALWAYS, FUNC_EQUAL, STENCILOP_INCREMENT, STENCILOP_REPLACE } 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, ELEMENTTYPE_IMAGE, ELEMENTTYPE_TEXT, FITMODE_STRETCH } from "./constants.js";
import { ImageElement } from "./image-element.js";
import { TextElement } from "./text-element.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();
class ElementComponent extends Component {
/**
* 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
*/
__publicField(this, "_evtLayersChanged", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerAdded", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerRemoved", null);
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);
this._modelTransform = new Mat4();
this._screenToWorld = new Mat4();
this._anchorTransform = new Mat4();
this._anchorDirty = true;
this._parentWorldTransform = new Mat4();
this._screenTransform = new Mat4();
this._screenCorners = [new Vec3(), new Vec3(), new Vec3(), new Vec3()];
this._canvasCorners = [new Vec2(), new Vec2(), new Vec2(), new Vec2()];
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();
this.screen = null;
this._type = ELEMENTTYPE_GROUP;
this._image = null;
this._text = null;
this._group = null;
this._drawOrder = 0;
this._fitMode = FITMODE_STRETCH;
this._useInput = false;
this._layers = [LAYERID_UI];
this._addedModels = [];
this._batchGroupId = -1;
this._offsetReadAt = 0;
this._maskOffset = 0.5;
this._maskedBy = 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) > 1e-3;
}
/**
* @type {boolean}
* @private
*/
get _hasSplitAnchorsY() {
return Math.abs(this._anchor.y - this._anchor.w) > 1e-3;
}
/**
* 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) {
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;
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 > 16777215) {
Debug.warn(`Element.drawOrder larger than max size of: ${16777215}`);
value = 16777215;
}
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);
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;
const p = this.entity.getLocalPosition();
const wl = this._absLeft;
const wr = this._localAnchor.z - value;
this._setWidth(wr - wl);
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];
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);
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);
matA.data[13] = -matA.data[13];
matA.mul2(this.screen.getWorldTransform(), matA);
for (let i = 0; i < 4; i++) {
matA.transformPoint(screenCorners[i], this._worldCorners[i]);
}
}
} else {
const localPos = this.entity.getLocalPosition();
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);
const entity = this.entity.parent ? this.entity.parent : this.entity;
matD.copy(entity.getWorldTransform());
matD.mul(matC).mul(matB).mul(matA);
vecA.set(localPos.x - this.pivot.x * this.calculatedWidth, localPos.y - this.pivot.y * this.calculatedHeight, localPos.z);
matD.transformPoint(vecA, this._worldCorners[0]);
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]);
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]);
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 of the element. This works for both {@link ELEMENTTYPE_IMAGE} and
* {@link ELEMENTTYPE_TEXT} element types.
*
* @type {number}
*/
set opacity(arg) {
this._setValue("opacity", arg);
}
/**
* Gets the opacity of the element.
*
* @type {number}
*/
get opacity() {
if (this._text) {
return this._text.opacity;
}
if (this._image) {
return this._image.opacity;
}
return null;
}
/**
* Sets the region of the texture to use in order to render an image. Values range from 0 to 1
* and indicate u, v, width, height. Only works for {@link ELEMENTTYPE_IMAGE} types.
*
* @type {Vec4}
*/
set rect(arg) {
this._setValue("rect", arg);
}
/**
* Gets the region of the texture to use in order to render an image.
*
* @type {Vec4}
*/
get rect() {
if (this._image) {
return this._image.rect;
}
return null;
}
/**
* Sets whether the Image Element should be treated as a mask. Masks do not render into the
* scene, but instead limit child elements to only be rendered where this element is rendered.
*
* @type {boolean}
*/
set mask(arg) {
this._setValue("mask", arg);
}
/**
* Gets whether the Image Element should be treated as a mask.
*
* @type {boolean}
*/
get mask() {
if (this._image) {
return this._image.mask;
}
return null;
}
/**
* Sets the text outline effect color and opacity. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {Color}
*/
set outlineColor(arg) {
this._setValue("outlineColor", arg);
}
/**
* Gets the text outline effect color and opacity.
*
* @type {Color}
*/
get outlineColor() {
if (this._text) {
return this._text.outlineColor;
}
return null;
}
/**
* Sets the width of the text outline effect. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/
set outlineThickness(arg) {
this._setValue("outlineThickness", arg);
}
/**
* Gets the width of the text outline effect.
*
* @type {number}
*/
get outlineThickness() {
if (this._text) {
return this._text.outlineThickness;
}
return null;
}
/**
* Sets the text shadow effect color and opacity. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {Color}
*/
set shadowColor(arg) {
this._setValue("shadowColor", arg);
}
/**
* Gets the text shadow effect color and opacity.
*
* @type {Color}
*/
get shadowColor() {
if (this._text) {
return this._text.shadowColor;
}
return null;
}
/**
* Sets the text shadow effect shift amount from original text. Only works for
* {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/
set shadowOffset(arg) {
this._setValue("shadowOffset", arg);
}
/**
* Gets the text shadow effect shift amount from original text.
*
* @type {number}
*/
get shadowOffset() {
if (this._text) {
return this._text.shadowOffset;
}
return null;
}
/**
* Sets whether markup processing is enabled for this element. Only works for
* {@link ELEMENTTYPE_TEXT} types. Defaults to false.
*
* @type {boolean}
*/
set enableMarkup(arg) {
this._setValue("enableMarkup", arg);
}
/**
* Gets whether markup processing is enabled for this element.
*
* @type {boolean}
*/
get enableMarkup() {
if (this._text) {
return this._text.enableMarkup;
}
return null;
}
/**
* Sets the index of the first character to render. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/
set rangeStart(arg) {
this._setValue("rangeStart", arg);
}
/**
* Gets the index of the first character to render.
*
* @type {number}
*/
get rangeStart() {
if (this._text) {
return this._text.rangeStart;
}
return null;
}
/**
* Sets the index of the last character to render. Only works for {@link ELEMENTTYPE_TEXT} types.
*
* @type {number}
*/
set rangeEnd(arg) {
this._setValue("rangeEnd", arg);
}
/**
* Gets the index of the last character to render.
*
* @type {number}
*/
get rangeEnd() {
if (this._text) {
return this._text.rangeEnd;
}
return null;
}
/** @ignore */
_setValue(name, value) {
if (this._text) {
if (this._text[name] !== value) {
this._dirtyBatch();
}
this._text[name] = value;
} else if (this._image) {
if (this._image[name] !== value) {
this._dirtyBatch();
}
this._image[name] = value;
}
}
_patch() {
this.entity._sync = this._sync;
this.entity.setPosition = this._setPosition;
this.entity.setLocalPosition = this._setLocalPosition;
}
_unpatch() {
this.entity._sync = Entity.prototype._sync;
this.entity.setPosition = Entity.prototype.setPosition;
this.entity.setLocalPosition = Entity.prototype.setLocalPosition;
}
/**
* Patched method for setting the position.
*
* @param {number|Vec3} x - The x coordinate or Vec3
* @param {number} [y] - The y coordinate
* @param {number} [z] - The z coordinate
* @private
*/
_setPosition(x, y, z) {
if (!this.element.screen) {
Entity.prototype.setPosition.call(this, x, y, z);
return;
}
if (x instanceof Vec3) {
position.copy(x);
} else {
position.set(x, y, z);
}
this.getWorldTransform();
invParentWtm.copy(this.element._screenToWorld).invert();
invParentWtm.transformPoint(position, this.localPosition);
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
/**
* Patched method for setting the local position.
*
* @param {number|Vec3} x - The x coordinate or Vec3
* @param {number} [y] - The y coordinate
* @param {number} [z] - The z coordinate
* @private
*/
_setLocalPosition(x, y, z) {
if (x instanceof Vec3) {
this.localPosition.copy(x);
} else {
this.localPosition.set(x, y, z);
}
const element = this.element;
const p = this.localPosition;
const pvt = element._pivot;
element._margin.x = p.x - element._calculatedWidth * pvt.x;
element._margin.z = element._localAnchor.z - element._localAnchor.x - element._calculatedWidth - element._margin.x;
element._margin.y = p.y - element._calculatedHeight * pvt.y;
element._margin.w = element._localAnchor.w - element._localAnchor.y - element._calculatedHeight - element._margin.y;
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
// this method overwrites GraphNode#sync and so operates in scope of the Entity.
_sync() {
const element = this.element;
const screen = element.screen;
if (screen) {
if (element._anchorDirty) {
let resx = 0;
let resy = 0;
let px = 0;
let py = 1;
if (this._parent && this._parent.element) {
resx = this._parent.element.calculatedWidth;
resy = this._parent.element.calculatedHeight;
px = this._parent.element.pivot.x;
py = this._parent.element.pivot.y;
} else {
const resolution = screen.screen.resolution;
resx = resolution.x / screen.screen.scale;
resy = resolution.y / screen.screen.scale;
}
element._anchorTransform.setTranslate(resx * (element.anchor.x - px), -(resy * (py - element.anchor.y)), 0);
element._anchorDirty = false;
element._calculateLocalAnchors();
}
if (element._sizeDirty) {
element._calculateSize(false, false);
}
}
if (this._dirtyLocal) {
this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale);
const p = this.localPosition;
const pvt = element._pivot;
element._margin.x = p.x - element._calculatedWidth * pvt.x;
element._margin.z = element._localAnchor.z - element._localAnchor.x - element._calculatedWidth - element._margin.x;
element._margin.y = p.y - element._calculatedHeight * pvt.y;
element._margin.w = element._localAnchor.w - element._localAnchor.y - element._calculatedHeight - element._margin.y;
this._dirtyLocal = false;
}
if (!screen) {
if (this._dirtyWorld) {
element._cornersDirty = true;
element._canvasCornersDirty = true;
element._worldCornersDirty = true;
}
Entity.prototype._sync.call(this);
return;
}
if (this._dirtyWorld) {
if (this._parent === null) {
this.worldTransform.copy(this.localTransform);
} else {
if (this._parent.element) {
element._screenToWorld.mul2(
this._parent.element._modelTransform,
element._anchorTransform
);
} else {
element._screenToWorld.copy(element._anchorTransform);
}
element._modelTransform.mul2(element._screenToWorld, this.localTransform);
if (screen) {
element._screenToWorld.mul2(screen.screen._screenMatrix, element._screenToWorld);
if (!screen.screen.screenSpace) {
element._screenToWorld.mul2(screen.worldTransform, element._screenToWorld);
}
this.worldTransform.mul2(element._screenToWorld, this.localTransform);
const parentWorldTransform = element._parentWorldTransform;
parentWorldTransform.setIdentity();
const parent = this._parent;
if (parent && parent.element && parent !== screen) {
matA.setTRS(Vec3.ZERO, parent.getLocalRotation(), parent.getLocalScale());
parentWorldTransform.mul2(parent.element._parentWorldTransform, matA);
}
const depthOffset = vecA;
depthOffset.set(0, 0, this.localPosition.z);
const pivotOffset = vecB;
pivotOffset.set(
element._absLeft + element._pivot.x * element.calculatedWidth,
element._absBottom + element._pivot.y * element.calculatedHeight,
0
);
matA.setTranslate(-pivotOffset.x, -pivotOffset.y, -pivotOffset.z);
matB.setTRS(depthOffset, this.getLocalRotation(), this.getLocalScale());
matC.setTranslate(pivotOffset.x, pivotOffset.y, pivotOffset.z);
element._screenTransform.mul2(element._parentWorldTransform, matC).mul(matB).mul(matA);
element._cornersDirty = true;
element._canvasCornersDirty = true;
element._worldCornersDirty = true;
} else {
this.worldTransform.copy(element._modelTransform);
}
}
this._dirtyWorld = false;
}
}
_onInsert(parent) {
const result = this._parseUpToScreen();
this.entity._dirtifyWorld();
this._updateScreen(result.screen);
this._dirtifyMask();
}
_dirtifyMask() {
let current = this.entity;
while (current) {
const next = current.parent;
if ((next === null || next.screen) && current.element) {
if (!this.system._prerender || !this.system._prerender.length) {
this.system._prerender = [];
this.system.app.once("prerender", this._onPrerender, this);
Debug.trace(TRACEID_ELEMENT, "register prerender");
}
const i = this.system._prerender.indexOf(this.entity);
if (i >= 0) {
this.system._prerender.splice(i, 1);
}
const j = this.system._prerender.indexOf(current);
if (j < 0) {
this.system._prerender.push(current);
}
Debug.trace(TRACEID_ELEMENT, `set prerender root to: ${current.name}`);
}
current = next;
}
}
_onPrerender() {
for (let i = 0; i < this.system._prerender.length; i++) {
const mask = this.system._prerender[i];
Debug.trace(TRACEID_ELEMENT, `prerender from: ${mask.name}`);
if (mask.element) {
const depth = 1;
mask.element.syncMask(depth);
}
}
this.system._prerender.length = 0;
}
_bindScreen(screen) {
screen._bindElement(this);
}
_unbindScreen(screen) {
screen._unbindElement(this);
}
_updateScreen(screen) {
if (this.screen && this.screen !== screen) {
this._unbindScreen(this.screen.screen);
}
const previousScreen = this.screen;
this.screen = screen;
if (this.screen) {
this._bindScreen(this.screen.screen);
}
this._calculateSize(this._hasSplitAnchorsX, this._hasSplitAnchorsY);
this.fire("set:screen", this.screen, previousScreen);
this._ancho