UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

1,473 lines (1,472 loc) 82.8 kB
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