import EventEmitter from 'eventemitter3'; import { Color } from '../../color/Color.mjs'; import { cullingMixin } from '../../culling/cullingMixin.mjs'; import { Matrix } from '../../maths/matrix/Matrix.mjs'; import { RAD_TO_DEG, DEG_TO_RAD } from '../../maths/misc/const.mjs'; import { ObservablePoint } from '../../maths/point/ObservablePoint.mjs'; import { uid } from '../../utils/data/uid.mjs'; import { deprecation, v8_0_0 } from '../../utils/logging/deprecation.mjs'; import { BigPool } from '../../utils/pool/PoolGroup.mjs'; import { childrenHelperMixin } from './container-mixins/childrenHelperMixin.mjs'; import { effectsMixin } from './container-mixins/effectsMixin.mjs'; import { findMixin } from './container-mixins/findMixin.mjs'; import { measureMixin } from './container-mixins/measureMixin.mjs'; import { onRenderMixin } from './container-mixins/onRenderMixin.mjs'; import { sortMixin } from './container-mixins/sortMixin.mjs'; import { toLocalGlobalMixin } from './container-mixins/toLocalGlobalMixin.mjs'; import { RenderGroup } from './RenderGroup.mjs'; import { assignWithIgnore } from './utils/assignWithIgnore.mjs'; "use strict"; const defaultSkew = new ObservablePoint(null); const defaultPivot = new ObservablePoint(null); const defaultScale = new ObservablePoint(null, 1, 1); const UPDATE_COLOR = 1; const UPDATE_BLEND = 2; const UPDATE_VISIBLE = 4; const UPDATE_TRANSFORM = 8; class Container extends EventEmitter { constructor(options = {}) { super(); /** unique id for this container */ this.uid = uid("renderable"); /** @private */ this._updateFlags = 15; // the render group this container owns /** @private */ this.renderGroup = null; // the render group this container belongs to /** @private */ this.parentRenderGroup = null; // the index of the container in the render group /** @private */ this.parentRenderGroupIndex = 0; // set to true if the container has changed. It is reset once the changes have been applied // by the transform system // its here to stop ensure that when things change, only one update gets registers with the transform system /** @private */ this.didChange = false; // same as above, but for the renderable /** @private */ this.didViewUpdate = false; // how deep is the container relative to its render group.. // unless the element is the root render group - it will be relative to its parent /** @private */ this.relativeRenderGroupDepth = 0; /** * The array of children of this container. * @readonly */ this.children = []; /** The display object container that contains this display object. */ this.parent = null; // used internally for changing up the render order.. mainly for masks and filters // TODO setting this should cause a rebuild?? /** @private */ this.includeInBuild = true; /** @private */ this.measurable = true; /** @private */ this.isSimple = true; // / /////////////Transform related props////////////// // used by the transform system to check if a container needs to be updated that frame // if the tick matches the current transform system tick, it is not updated again /** * @internal * @ignore */ this.updateTick = -1; /** * Current transform of the object based on local factors: position, scale, other stuff. * @readonly */ this.localTransform = new Matrix(); /** * The relative group transform is a transform relative to the render group it belongs too. It will include all parent * transforms and up to the render group (think of it as kind of like a stage - but the stage can be nested). * If this container is is self a render group matrix will be relative to its parent render group * @readonly */ this.relativeGroupTransform = new Matrix(); /** * The group transform is a transform relative to the render group it belongs too. * If this container is render group then this will be an identity matrix. other wise it * will be the same as the relativeGroupTransform. * Use this value when actually rendering things to the screen * @readonly */ this.groupTransform = this.relativeGroupTransform; /** If the object has been destroyed via destroy(). If true, it should not be used. */ this.destroyed = false; // transform data.. /** * The coordinate of the object relative to the local coordinates of the parent. * @internal * @ignore */ this._position = new ObservablePoint(this, 0, 0); /** * The scale factor of the object. * @internal * @ignore */ this._scale = defaultScale; /** * The pivot point of the container that it rotates around. * @internal * @ignore */ this._pivot = defaultPivot; /** * The skew amount, on the x and y axis. * @internal * @ignore */ this._skew = defaultSkew; /** * The X-coordinate value of the normalized local X axis, * the first column of the local transformation matrix without a scale. * @internal * @ignore */ this._cx = 1; /** * The Y-coordinate value of the normalized local X axis, * the first column of the local transformation matrix without a scale. * @internal * @ignore */ this._sx = 0; /** * The X-coordinate value of the normalized local Y axis, * the second column of the local transformation matrix without a scale. * @internal * @ignore */ this._cy = 0; /** * The Y-coordinate value of the normalized local Y axis, * the second column of the local transformation matrix without a scale. * @internal * @ignore */ this._sy = 1; /** * The rotation amount. * @internal * @ignore */ this._rotation = 0; // / COLOR related props ////////////// // color stored as ABGR this.localColor = 16777215; this.localAlpha = 1; this.groupAlpha = 1; // A this.groupColor = 16777215; // BGR this.groupColorAlpha = 4294967295; // ABGR // / BLEND related props ////////////// /** * @internal * @ignore */ this.localBlendMode = "inherit"; /** * @internal * @ignore */ this.groupBlendMode = "normal"; // / VISIBILITY related props ////////////// // visibility // 0b11 // first bit is visible, second bit is renderable /** * This property holds three bits: culled, visible, renderable * the third bit represents culling (0 = culled, 1 = not culled) 0b100 * the second bit represents visibility (0 = not visible, 1 = visible) 0b010 * the first bit represents renderable (0 = not renderable, 1 = renderable) 0b001 * @internal * @ignore */ this.localDisplayStatus = 7; // 0b11 | 0b10 | 0b01 | 0b00 /** * @internal * @ignore */ this.globalDisplayStatus = 7; /** * A value that increments each time the containe is modified * eg children added, removed etc * @ignore */ this._didContainerChangeTick = 0; /** * A value that increments each time the container view is modified * eg texture swap, geometry change etc * @ignore */ this._didViewChangeTick = 0; /** * property that tracks if the container transform has changed * @ignore */ this._didLocalTransformChangeId = -1; this.effects = []; assignWithIgnore(this, options, { children: true, parent: true, effects: true }); options.children?.forEach((child) => this.addChild(child)); options.parent?.addChild(this); } /** * Mixes all enumerable properties and methods from a source object to Container. * @param source - The source of properties and methods to mix in. */ static mixin(source) { Object.defineProperties(Container.prototype, Object.getOwnPropertyDescriptors(source)); } /** * We now use the _didContainerChangeTick and _didViewChangeTick to track changes * @deprecated since 8.2.6 * @ignore */ set _didChangeId(value) { this._didViewChangeTick = value >> 12 & 4095; this._didContainerChangeTick = value & 4095; } get _didChangeId() { return this._didContainerChangeTick & 4095 | (this._didViewChangeTick & 4095) << 12; } /** * Adds one or more children to the container. * * Multiple items can be added like so: `myContainer.addChild(thingOne, thingTwo, thingThree)` * @param {...Container} children - The Container(s) to add to the container * @returns {Container} - The first child that was added. */ addChild(...children) { if (!this.allowChildren) { deprecation(v8_0_0, "addChild: Only Containers will be allowed to add children in v8.0.0"); } if (children.length > 1) { for (let i = 0; i < children.length; i++) { this.addChild(children[i]); } return children[0]; } const child = children[0]; if (child.parent === this) { this.children.splice(this.children.indexOf(child), 1); this.children.push(child); if (this.parentRenderGroup) { this.parentRenderGroup.structureDidChange = true; } return child; } if (child.parent) { child.parent.removeChild(child); } this.children.push(child); if (this.sortableChildren) this.sortDirty = true; child.parent = this; child.didChange = true; child.didViewUpdate = false; child._updateFlags = 15; const renderGroup = this.renderGroup || this.parentRenderGroup; if (renderGroup) { renderGroup.addChild(child); } this.emit("childAdded", child, this, this.children.length - 1); child.emit("added", this); this._didViewChangeTick++; if (child._zIndex !== 0) { child.depthOfChildModified(); } return child; } /** * Removes one or more children from the container. * @param {...Container} children - The Container(s) to remove * @returns {Container} The first child that was removed. */ removeChild(...children) { if (children.length > 1) { for (let i = 0; i < children.length; i++) { this.removeChild(children[i]); } return children[0]; } const child = children[0]; const index = this.children.indexOf(child); if (index > -1) { this._didViewChangeTick++; this.children.splice(index, 1); if (this.renderGroup) { this.renderGroup.removeChild(child); } else if (this.parentRenderGroup) { this.parentRenderGroup.removeChild(child); } child.parent = null; this.emit("childRemoved", child, this, index); child.emit("removed", this); } return child; } /** @ignore */ _onUpdate(point) { if (point) { if (point === this._skew) { this._updateSkew(); } } this._didContainerChangeTick++; if (this.didChange) return; this.didChange = true; if (this.parentRenderGroup) { this.parentRenderGroup.onChildUpdate(this); } } set isRenderGroup(value) { if (!!this.renderGroup === value) return; if (value) { this.enableRenderGroup(); } else { this.disableRenderGroup(); } } /** * Returns true if this container is a render group. * This means that it will be rendered as a separate pass, with its own set of instructions */ get isRenderGroup() { return !!this.renderGroup; } /** * Calling this enables a render group for this container. * This means it will be rendered as a separate set of instructions. * The transform of the container will also be handled on the GPU rather than the CPU. */ enableRenderGroup() { if (this.renderGroup) return; const parentRenderGroup = this.parentRenderGroup; parentRenderGroup?.removeChild(this); this.renderGroup = BigPool.get(RenderGroup, this); this.groupTransform = Matrix.IDENTITY; parentRenderGroup?.addChild(this); this._updateIsSimple(); } /** This will disable the render group for this container. */ disableRenderGroup() { if (!this.renderGroup) return; const parentRenderGroup = this.parentRenderGroup; parentRenderGroup?.removeChild(this); BigPool.return(this.renderGroup); this.renderGroup = null; this.groupTransform = this.relativeGroupTransform; parentRenderGroup?.addChild(this); this._updateIsSimple(); } /** @ignore */ _updateIsSimple() { this.isSimple = !this.renderGroup && this.effects.length === 0; } /** * Current transform of the object based on world (parent) factors. * @readonly */ get worldTransform() { this._worldTransform || (this._worldTransform = new Matrix()); if (this.renderGroup) { this._worldTransform.copyFrom(this.renderGroup.worldTransform); } else if (this.parentRenderGroup) { this._worldTransform.appendFrom(this.relativeGroupTransform, this.parentRenderGroup.worldTransform); } return this._worldTransform; } // / ////// transform related stuff /** * The position of the container on the x axis relative to the local coordinates of the parent. * An alias to position.x */ get x() { return this._position.x; } set x(value) { this._position.x = value; } /** * The position of the container on the y axis relative to the local coordinates of the parent. * An alias to position.y */ get y() { return this._position.y; } set y(value) { this._position.y = value; } /** * The coordinate of the object relative to the local coordinates of the parent. * @since 4.0.0 */ get position() { return this._position; } set position(value) { this._position.copyFrom(value); } /** * The rotation of the object in radians. * 'rotation' and 'angle' have the same effect on a display object; rotation is in radians, angle is in degrees. */ get rotation() { return this._rotation; } set rotation(value) { if (this._rotation !== value) { this._rotation = value; this._onUpdate(this._skew); } } /** * The angle of the object in degrees. * 'rotation' and 'angle' have the same effect on a display object; rotation is in radians, angle is in degrees. */ get angle() { return this.rotation * RAD_TO_DEG; } set angle(value) { this.rotation = value * DEG_TO_RAD; } /** * The center of rotation, scaling, and skewing for this display object in its local space. The `position` * is the projection of `pivot` in the parent's local space. * * By default, the pivot is the origin (0, 0). * @since 4.0.0 */ get pivot() { if (this._pivot === defaultPivot) { this._pivot = new ObservablePoint(this, 0, 0); } return this._pivot; } set pivot(value) { if (this._pivot === defaultPivot) { this._pivot = new ObservablePoint(this, 0, 0); } typeof value === "number" ? this._pivot.set(value) : this._pivot.copyFrom(value); } /** * The skew factor for the object in radians. * @since 4.0.0 */ get skew() { if (this._skew === defaultSkew) { this._skew = new ObservablePoint(this, 0, 0); } return this._skew; } set skew(value) { if (this._skew === defaultSkew) { this._skew = new ObservablePoint(this, 0, 0); } this._skew.copyFrom(value); } /** * The scale factors of this object along the local coordinate axes. * * The default scale is (1, 1). * @since 4.0.0 */ get scale() { if (this._scale === defaultScale) { this._scale = new ObservablePoint(this, 1, 1); } return this._scale; } set scale(value) { if (this._scale === defaultScale) { this._scale = new ObservablePoint(this, 0, 0); } typeof value === "number" ? this._scale.set(value) : this._scale.copyFrom(value); } /** * The width of the Container, setting this will actually modify the scale to achieve the value set. * @memberof scene.Container# */ get width() { return Math.abs(this.scale.x * this.getLocalBounds().width); } set width(value) { const localWidth = this.getLocalBounds().width; this._setWidth(value, localWidth); } /** * The height of the Container, setting this will actually modify the scale to achieve the value set. * @memberof scene.Container# */ get height() { return Math.abs(this.scale.y * this.getLocalBounds().height); } set height(value) { const localHeight = this.getLocalBounds().height; this._setHeight(value, localHeight); } /** * Retrieves the size of the container as a [Size]{@link Size} object. * This is faster than get the width and height separately. * @param out - Optional object to store the size in. * @returns - The size of the container. * @memberof scene.Container# */ getSize(out) { if (!out) { out = {}; } const bounds = this.getLocalBounds(); out.width = Math.abs(this.scale.x * bounds.width); out.height = Math.abs(this.scale.y * bounds.height); return out; } /** * Sets the size of the container to the specified width and height. * This is faster than setting the width and height separately. * @param value - This can be either a number or a [Size]{@link Size} object. * @param height - The height to set. Defaults to the value of `width` if not provided. * @memberof scene.Container# */ setSize(value, height) { const size = this.getLocalBounds(); if (typeof value === "object") { height = value.height ?? value.width; value = value.width; } else { height ?? (height = value); } value !== void 0 && this._setWidth(value, size.width); height !== void 0 && this._setHeight(height, size.height); } /** Called when the skew or the rotation changes. */ _updateSkew() { const rotation = this._rotation; const skew = this._skew; this._cx = Math.cos(rotation + skew._y); this._sx = Math.sin(rotation + skew._y); this._cy = -Math.sin(rotation - skew._x); this._sy = Math.cos(rotation - skew._x); } /** * Updates the transform properties of the container (accepts partial values). * @param {object} opts - The options for updating the transform. * @param {number} opts.x - The x position of the container. * @param {number} opts.y - The y position of the container. * @param {number} opts.scaleX - The scale factor on the x-axis. * @param {number} opts.scaleY - The scale factor on the y-axis. * @param {number} opts.rotation - The rotation of the container, in radians. * @param {number} opts.skewX - The skew factor on the x-axis. * @param {number} opts.skewY - The skew factor on the y-axis. * @param {number} opts.pivotX - The x coordinate of the pivot point. * @param {number} opts.pivotY - The y coordinate of the pivot point. */ updateTransform(opts) { this.position.set( typeof opts.x === "number" ? opts.x : this.position.x, typeof opts.y === "number" ? opts.y : this.position.y ); this.scale.set( typeof opts.scaleX === "number" ? opts.scaleX || 1 : this.scale.x, typeof opts.scaleY === "number" ? opts.scaleY || 1 : this.scale.y ); this.rotation = typeof opts.rotation === "number" ? opts.rotation : this.rotation; this.skew.set( typeof opts.skewX === "number" ? opts.skewX : this.skew.x, typeof opts.skewY === "number" ? opts.skewY : this.skew.y ); this.pivot.set( typeof opts.pivotX === "number" ? opts.pivotX : this.pivot.x, typeof opts.pivotY === "number" ? opts.pivotY : this.pivot.y ); return this; } /** * Updates the local transform using the given matrix. * @param matrix - The matrix to use for updating the transform. */ setFromMatrix(matrix) { matrix.decompose(this); } /** Updates the local transform. */ updateLocalTransform() { const localTransformChangeId = this._didContainerChangeTick; if (this._didLocalTransformChangeId === localTransformChangeId) return; this._didLocalTransformChangeId = localTransformChangeId; const lt = this.localTransform; const scale = this._scale; const pivot = this._pivot; const position = this._position; const sx = scale._x; const sy = scale._y; const px = pivot._x; const py = pivot._y; lt.a = this._cx * sx; lt.b = this._sx * sx; lt.c = this._cy * sy; lt.d = this._sy * sy; lt.tx = position._x - (px * lt.a + py * lt.c); lt.ty = position._y - (px * lt.b + py * lt.d); } // / ///// color related stuff set alpha(value) { if (value === this.localAlpha) return; this.localAlpha = value; this._updateFlags |= UPDATE_COLOR; this._onUpdate(); } /** The opacity of the object. */ get alpha() { return this.localAlpha; } set tint(value) { const tempColor = Color.shared.setValue(value ?? 16777215); const bgr = tempColor.toBgrNumber(); if (bgr === this.localColor) return; this.localColor = bgr; this._updateFlags |= UPDATE_COLOR; this._onUpdate(); } /** * The tint applied to the sprite. This is a hex value. * * A value of 0xFFFFFF will remove any tint effect. * @default 0xFFFFFF */ get tint() { const bgr = this.localColor; return ((bgr & 255) << 16) + (bgr & 65280) + (bgr >> 16 & 255); } // / //////////////// blend related stuff set blendMode(value) { if (this.localBlendMode === value) return; if (this.parentRenderGroup) { this.parentRenderGroup.structureDidChange = true; } this._updateFlags |= UPDATE_BLEND; this.localBlendMode = value; this._onUpdate(); } /** * The blend mode to be applied to the sprite. Apply a value of `'normal'` to reset the blend mode. * @default 'normal' */ get blendMode() { return this.localBlendMode; } // / ///////// VISIBILITY / RENDERABLE ///////////////// /** The visibility of the object. If false the object will not be drawn, and the transform will not be updated. */ get visible() { return !!(this.localDisplayStatus & 2); } set visible(value) { const valueNumber = value ? 2 : 0; if ((this.localDisplayStatus & 2) === valueNumber) return; if (this.parentRenderGroup) { this.parentRenderGroup.structureDidChange = true; } this._updateFlags |= UPDATE_VISIBLE; this.localDisplayStatus ^= 2; this._onUpdate(); } /** @ignore */ get culled() { return !(this.localDisplayStatus & 4); } /** @ignore */ set culled(value) { const valueNumber = value ? 0 : 4; if ((this.localDisplayStatus & 4) === valueNumber) return; if (this.parentRenderGroup) { this.parentRenderGroup.structureDidChange = true; } this._updateFlags |= UPDATE_VISIBLE; this.localDisplayStatus ^= 4; this._onUpdate(); } /** Can this object be rendered, if false the object will not be drawn but the transform will still be updated. */ get renderable() { return !!(this.localDisplayStatus & 1); } set renderable(value) { const valueNumber = value ? 1 : 0; if ((this.localDisplayStatus & 1) === valueNumber) return; this._updateFlags |= UPDATE_VISIBLE; this.localDisplayStatus ^= 1; if (this.parentRenderGroup) { this.parentRenderGroup.structureDidChange = true; } this._onUpdate(); } /** Whether or not the object should be rendered. */ get isRenderable() { return this.localDisplayStatus === 7 && this.groupAlpha > 0; } /** * Removes all internal references and listeners as well as removes children from the display list. * Do not use a Container after calling `destroy`. * @param options - Options parameter. A boolean will act as if all options * have been set to that value * @param {boolean} [options.children=false] - if set to true, all the children will have their destroy * method called as well. 'options' will be passed on to those calls. * @param {boolean} [options.texture=false] - Only used for children with textures e.g. Sprites. If options.children * is set to true it should destroy the texture of the child sprite * @param {boolean} [options.textureSource=false] - Only used for children with textures e.g. Sprites. * If options.children is set to true it should destroy the texture source of the child sprite * @param {boolean} [options.context=false] - Only used for children with graphicsContexts e.g. Graphics. * If options.children is set to true it should destroy the context of the child graphics */ destroy(options = false) { if (this.destroyed) return; this.destroyed = true; const oldChildren = this.removeChildren(0, this.children.length); this.removeFromParent(); this.parent = null; this._maskEffect = null; this._filterEffect = null; this.effects = null; this._position = null; this._scale = null; this._pivot = null; this._skew = null; this.emit("destroyed", this); this.removeAllListeners(); const destroyChildren = typeof options === "boolean" ? options : options?.children; if (destroyChildren) { for (let i = 0; i < oldChildren.length; ++i) { oldChildren[i].destroy(options); } } this.renderGroup?.destroy(); this.renderGroup = null; } } Container.mixin(childrenHelperMixin); Container.mixin(toLocalGlobalMixin); Container.mixin(onRenderMixin); Container.mixin(measureMixin); Container.mixin(effectsMixin); Container.mixin(findMixin); Container.mixin(sortMixin); Container.mixin(cullingMixin); export { Container, UPDATE_BLEND, UPDATE_COLOR, UPDATE_TRANSFORM, UPDATE_VISIBLE }; //# sourceMappingURL=Container.mjs.map