import {StateProperty, Trait} from '@avocado/traits'; import {Rectangle, Vector} from '@avocado/math'; import {compose} from '@latus/core'; import Container from '../container'; // eslint-disable-next-line no-bitwise const AUTO_ZINDEX = 1 << 16; const decorate = compose( StateProperty('isVisible', { track: true, }), StateProperty('opacity', { track: true, }), StateProperty('rotation', { track: true, }), StateProperty('visibleScale', { track: true, }), StateProperty('zIndex', { track: true, }), ); export default () => class Visible extends decorate(Trait) { #container; #rawVisibleAabb = [0, 0, 0, 0]; #scheduledBoundingBoxUpdate = true; #usingAutoZIndex = true; #visibleScale = [0, 0]; constructor() { super(); if ('client' === process.env.SIDE) { this.#container = new Container(); } ({ visibleScale: this.#visibleScale, } = this.constructor.defaultState()); } acceptPacket(packet) { if ('TraitUpdateVisible' === packet.constructor.type) { this.entity.isVisible = packet.data.isVisible; this.entity.opacity = packet.data.opacity; } } static behaviorTypes() { return { updateVisibleBoundingBox: { advanced: true, type: 'void', label: 'Update the visible bounding box.', }, }; } static defaultParams() { return { filter: undefined, }; } static defaultState() { return { isVisible: true, opacity: 1, rotation: 0, visibleScale: [1, 1], zIndex: AUTO_ZINDEX, }; } static describeParams() { return { filter: { type: 'object', label: 'Filter', }, }; } static describeState() { return { isVisible: { type: 'bool', label: 'Is visible', }, opacity: { type: 'number', label: 'Opacity', }, rotation: { type: 'number', label: 'Rotation', }, visibleScale: { type: 'vector', label: 'Scale', }, zIndex: { type: 'number', label: 'Z index', }, }; } destroy() { if (this.#container) { this.#container.destroy(); } } get container() { return this.#container; } forceUpdateBoundingBox() { // Collect all bounding boxes. const visibleAabbs = this.entity.invokeHookFlat('visibleAabbs'); if (0 === visibleAabbs.length) { this.#rawVisibleAabb = [0, 0, 0, 0]; } else { let unifiedBoundingBox = [0, 0, 0, 0]; for (let i = 0; i < visibleAabbs.length; ++i) { const visibleAabb = visibleAabbs[i]; unifiedBoundingBox = Rectangle.united( unifiedBoundingBox, visibleAabb, ); } this.#rawVisibleAabb = unifiedBoundingBox; } this.translateVisibleAabb(); this.#scheduledBoundingBoxUpdate = false; } listeners() { return { isVisibleChanged: () => { if (!this.#container) { return; } this.#container.visible = this.entity.isVisible; }, opacityChanged: () => { if (!this.#container) { return; } this.#container.alpha = this.entity.opacity; }, positionChanged: () => { this.translateVisibleAabb(); }, rotationChanged: () => { if (!this.#container) { return; } this.#container.rotation = this.entity.rotation; }, traitAdded: () => { this.synchronizePosition(); }, updateVisibleBoundingBox: () => { this.#scheduledBoundingBoxUpdate = true; }, visibleScaleChanged: () => { this.#visibleScale = Vector.copy(this.entity.visibleScale); if (this.#container) { this.#container.scale = this.#visibleScale; } }, zIndexChanged: (old, zIndex) => { this.onZIndexChanged(zIndex); }, }; } load(json) { super.load(json); if ('client' === process.env.SIDE) { const {filter} = this.params; if (filter) { this.#container.setFilter(filter); } this.#container.isVisible = this.state.isVisible; } this.#visibleScale = Vector.copy(this.state.visibleScale); this.onZIndexChanged(this.state.zIndex); this.entity.emit('updateVisibleBoundingBox'); } methods() { return { forceUpdateBoundingBox: () => { this.forceUpdateBoundingBox(); }, }; } onZIndexChanged(zIndex) { this.#usingAutoZIndex = AUTO_ZINDEX === zIndex; if (!this.#usingAutoZIndex) { if (this.#container) { this.#container.zIndex = zIndex; } } } packets() { const {isVisible, opacity} = this.stateDifferences(); if (isVisible || opacity) { return [[ 'TraitUpdateVisible', { isVisible: this.state.isVisible, opacity: this.state.opacity, }, ]]; } return []; } get rawVisibleScale() { return this.#visibleScale; } set rawVisibleScale(scale) { this.entity.visibleScale = scale; } renderTick() { this.synchronizePosition(); } synchronizePosition() { if (!this.entity.is('Positioned')) { return; } if (!this.#container) { return; } this.#container.position = this.entity.position; if (this.#usingAutoZIndex) { if (this.#container) { this.#container.zIndex = this.entity.y; } } } get visibleScaleX() { return this.state.visibleScale[0]; } set visibleScaleX(x) { this.rawVisibleScale = [x, this.visibleScaleY]; } get visibleScaleY() { return this.state.visibleScale[1]; } set visibleScaleY(y) { this.rawVisibleScale = [this.visibleScaleX, y]; } tick() { if ('client' !== process.env.SIDE) { if (this.#scheduledBoundingBoxUpdate) { this.forceUpdateBoundingBox(); } } } translateVisibleAabb() { const visibleAabb = Rectangle.translated( this.#rawVisibleAabb, this.entity.position, ); /* eslint-disable prefer-destructuring */ this.entity.visibleAabb[0] = visibleAabb[0]; this.entity.visibleAabb[1] = visibleAabb[1]; this.entity.visibleAabb[2] = visibleAabb[2]; this.entity.visibleAabb[3] = visibleAabb[3]; /* eslint-enable prefer-destructuring */ this.entity.emit('visibleAabbChanged'); } };