UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

311 lines (308 loc) 11.6 kB
import { Debug } from '../../../core/debug.js'; import { math } from '../../../core/math/math.js'; import { Mat4 } from '../../../core/math/mat4.js'; import { Vec2 } from '../../../core/math/vec2.js'; import { Entity } from '../../entity.js'; import { SCALEMODE_NONE, SCALEMODE_BLEND } from './constants.js'; import { Component } from '../component.js'; /** * @import { ScreenComponentSystem } from './system.js' */ const _transform = new Mat4(); /** * A ScreenComponent defines a rectangular area where user interfaces can be constructed. Screens * can either be 2D (screen space) or 3D (world space) - see {@link screenSpace}. It is possible to * create an {@link Entity} hierarchy underneath an Entity with a ScreenComponent to create complex * user interfaces using the following components: * * - {@link ButtonComponent} * - {@link ElementComponent} * - {@link LayoutChildComponent} * - {@link LayoutGroupComponent} * - {@link ScrollbarComponent} * - {@link ScrollViewComponent} * * You should never need to use the ScreenComponent constructor directly. To add a ScreenComponent * to an {@link Entity}, use {@link Entity#addComponent}: * * ```javascript * const entity = new pc.Entity(); * entity.addComponent('screen', { * referenceResolution: new pc.Vec2(1280, 720), * screenSpace: false * }); * ``` * * Once the ScreenComponent is added to the entity, you can access it via the {@link Entity#screen} * property: * * ```javascript * entity.screen.scaleBlend = 0.6; // Set the screen's scale blend to 0.6 * * console.log(entity.screen.scaleBlend); // Get the screen's scale blend and print it * ``` * * Relevant Engine API examples: * * - [Screen Space Screen](https://playcanvas.github.io/#/user-interface/text) * - [World Space Screen](https://playcanvas.github.io/#/user-interface/world-ui) * * @hideconstructor * @category User Interface */ class ScreenComponent extends Component { /** * Create a new ScreenComponent. * * @param {ScreenComponentSystem} system - The ComponentSystem that created this Component. * @param {Entity} entity - The Entity that this Component is attached to. */ constructor(system, entity){ super(system, entity); this._resolution = new Vec2(640, 320); this._referenceResolution = new Vec2(640, 320); this._scaleMode = SCALEMODE_NONE; this.scale = 1; this._scaleBlend = 0.5; this._priority = 0; this._screenSpace = false; /** * If true, then elements inside this screen will be not be rendered when outside of the * screen (only valid when screenSpace is true). * * @type {boolean} */ this.cull = this._screenSpace; this._screenMatrix = new Mat4(); this._elements = new Set(); system.app.graphicsDevice.on('resizecanvas', this._onResize, this); } /** * Set the drawOrder of each child {@link ElementComponent} so that ElementComponents which are * last in the hierarchy are rendered on top. Draw Order sync is queued and will be updated by * the next update loop. */ syncDrawOrder() { this.system.queueDrawOrderSync(this.entity.getGuid(), this._processDrawOrderSync, this); } _recurseDrawOrderSync(e, i) { if (!(e instanceof Entity)) { return i; } if (e.element) { const prevDrawOrder = e.element.drawOrder; e.element.drawOrder = i++; if (e.element._batchGroupId >= 0 && prevDrawOrder !== e.element.drawOrder) { this.system.app.batcher?.markGroupDirty(e.element._batchGroupId); } } // child particle system inside 2D screen sub-hierarchy gets sorted along other 2D elements if (e.particlesystem) { e.particlesystem.drawOrder = i++; } const children = e.children; for(let j = 0; j < children.length; j++){ i = this._recurseDrawOrderSync(children[j], i); } return i; } _processDrawOrderSync() { const i = 1; this._recurseDrawOrderSync(this.entity, i); // fire internal event after all screen hierarchy is synced this.fire('syncdraworder'); } _calcProjectionMatrix() { const w = this._resolution.x / this.scale; const h = this._resolution.y / this.scale; const left = 0; const right = w; const bottom = -h; const top = 0; const near = 1; const far = -1; this._screenMatrix.setOrtho(left, right, bottom, top, near, far); if (!this._screenSpace) { _transform.setScale(0.5 * w, 0.5 * h, 1); this._screenMatrix.mul2(_transform, this._screenMatrix); } } _updateScale() { this.scale = this._calcScale(this._resolution, this.referenceResolution); } _calcScale(resolution, referenceResolution) { // Using log of scale values // This produces a nicer outcome where if you have a xscale = 2 and yscale = 0.5 // the combined scale is 1 for an even blend const lx = Math.log2((resolution.x || 1) / referenceResolution.x); const ly = Math.log2((resolution.y || 1) / referenceResolution.y); return Math.pow(2, lx * (1 - this._scaleBlend) + ly * this._scaleBlend); } _onResize(width, height) { if (this._screenSpace) { this._resolution.set(width, height); this.resolution = this._resolution; // force update } } _bindElement(element) { this._elements.add(element); } _unbindElement(element) { this._elements.delete(element); } onRemove() { this.system.app.graphicsDevice.off('resizecanvas', this._onResize, this); this.fire('remove'); this._elements.forEach((element)=>element._onScreenRemove()); this._elements.clear(); // remove all events this.off(); } /** * Sets the width and height of the ScreenComponent. When {@link screenSpace} is true, the * resolution will always be equal to {@link GraphicsDevice#width} by * {@link GraphicsDevice#height}. * * @type {Vec2} */ set resolution(value) { if (!this._screenSpace) { this._resolution.set(value.x, value.y); } else { // ignore input when using screen space. this._resolution.set(this.system.app.graphicsDevice.width, this.system.app.graphicsDevice.height); } this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:resolution', this._resolution); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } /** * Gets the width and height of the ScreenComponent. * * @type {Vec2} */ get resolution() { return this._resolution; } /** * Sets the resolution that the ScreenComponent is designed for. This is only taken into * account when {@link screenSpace} is true and {@link scaleMode} is {@link SCALEMODE_BLEND}. * If the actual resolution is different, then the ScreenComponent will be scaled according to * the {@link scaleBlend} value. * * @type {Vec2} */ set referenceResolution(value) { this._referenceResolution.set(value.x, value.y); this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:referenceresolution', this._resolution); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } /** * Gets the resolution that the ScreenComponent is designed for. * * @type {Vec2} */ get referenceResolution() { if (this._scaleMode === SCALEMODE_NONE) { return this._resolution; } return this._referenceResolution; } /** * Sets whether the ScreenComponent will render its child {@link ElementComponent}s in screen * space instead of world space. Enable this to create 2D user interfaces. Defaults to false. * * @type {boolean} */ set screenSpace(value) { this._screenSpace = value; if (this._screenSpace) { this._resolution.set(this.system.app.graphicsDevice.width, this.system.app.graphicsDevice.height); } this.resolution = this._resolution; // force update either way if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:screenspace', this._screenSpace); this._elements.forEach((element)=>element._onScreenSpaceChange()); } /** * Gets whether the ScreenComponent will render its child {@link ElementComponent}s in screen * space instead of world space. * * @type {boolean} */ get screenSpace() { return this._screenSpace; } /** * Sets the scale mode. Can either be {@link SCALEMODE_NONE} or {@link SCALEMODE_BLEND}. See * the description of {@link referenceResolution} for more information. Defaults to * {@link SCALEMODE_NONE}. * * @type {string} */ set scaleMode(value) { if (value !== SCALEMODE_NONE && value !== SCALEMODE_BLEND) { value = SCALEMODE_NONE; } // world space screens do not support scale modes if (!this._screenSpace && value !== SCALEMODE_NONE) { value = SCALEMODE_NONE; } this._scaleMode = value; this.resolution = this._resolution; // force update this.fire('set:scalemode', this._scaleMode); } /** * Gets the scale mode. * * @type {string} */ get scaleMode() { return this._scaleMode; } /** * Sets the scale blend. This is a value between 0 and 1 that is used when {@link scaleMode} is * equal to {@link SCALEMODE_BLEND}. Scales the ScreenComponent with width as a reference (when * value is 0), the height as a reference (when value is 1) or anything in between. Defaults to * 0.5. * * @type {number} */ set scaleBlend(value) { this._scaleBlend = value; this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:scaleblend', this._scaleBlend); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } /** * Gets the scale blend. * * @type {number} */ get scaleBlend() { return this._scaleBlend; } /** * Sets the screen's render priority. Priority determines the order in which ScreenComponents * in the same layer are rendered. Number must be an integer between 0 and 127. Priority is set * into the top 8 bits of the {@link ElementComponent#drawOrder} property. Defaults to 0. * * @type {number} */ set priority(value) { Debug.assert(value >= 0 && value <= 0x7F, `Screen priority must be between 0 and 127, got ${value}`); value = math.clamp(value, 0, 0x7F); if (this._priority === value) { return; } this._priority = value; this.syncDrawOrder(); } /** * Gets the screen's render priority. * * @type {number} */ get priority() { return this._priority; } } export { ScreenComponent };