/** * A mixin which decorates a DisplayObject with depth and/or occlusion properties. * @category - Mixins * @param {typeof PIXI.DisplayObject} DisplayObject The parent DisplayObject class being mixed * @returns {typeof PrimaryOccludableObject} A DisplayObject subclass mixed with OccludableObject features * @mixin */ function PrimaryOccludableObjectMixin(DisplayObject) { class PrimaryOccludableObject extends PrimaryCanvasObjectMixin(DisplayObject) { /** * Restrictions options packed into a single value with bitwise logic. * @type {foundry.utils.BitMask} */ #restrictionState = new foundry.utils.BitMask({ light: false, weather: false }); /** * Is this occludable object hidden for Gamemaster visibility only? * @type {boolean} */ hidden = false; /** * A flag which tracks whether the primary canvas object is currently in an occluded state. * @type {boolean} */ occluded = false; /** * The occlusion mode of this occludable object. * @type {number} */ occlusionMode = CONST.OCCLUSION_MODES.NONE; /** * The unoccluded alpha of this object. * @type {number} */ unoccludedAlpha = 1; /** * The occlusion alpha of this object. * @type {number} */ occludedAlpha = 0; /** * Fade this object on hover? * @type {boolean} * @defaultValue true */ get hoverFade() { return this.#hoverFade; } set hoverFade(value) { if ( this.#hoverFade === value ) return; this.#hoverFade = value; const state = this._hoverFadeState; state.hovered = false; state.faded = false; state.fading = false; state.occlusion = 0; } /** * Fade this object on hover? * @type {boolean} */ #hoverFade = true; /** * @typedef {object} OcclusionState * @property {number} fade The amount of FADE occlusion * @property {number} radial The amount of RADIAL occlusion * @property {number} vision The amount of VISION occlusion */ /** * The amount of rendered FADE, RADIAL, and VISION occlusion. * @type {OcclusionState} * @internal */ _occlusionState = { fade: 0.0, radial: 0.0, vision: 0.0 }; /** * @typedef {object} HoverFadeState * @property {boolean} hovered The hovered state * @property {number} hoveredTime The last time when a mouse event was hovering this object * @property {boolean} faded The faded state * @property {boolean} fading The fading state * @property {number} fadingTime The time the fade animation started * @property {number} occlusion The amount of occlusion */ /** * The state of hover-fading. * @type {HoverFadeState} * @internal */ _hoverFadeState = { hovered: false, hoveredTime: 0, faded: false, fading: false, fadingTime: 0, occlusion: 0.0 }; /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /** * Get the blocking option bitmask value. * @returns {number} * @internal */ get _restrictionState() { return this.#restrictionState.valueOf(); } /* -------------------------------------------- */ /** * Is this object blocking light? * @type {boolean} */ get restrictsLight() { return this.#restrictionState.hasState(this.#restrictionState.states.light); } set restrictsLight(enabled) { this.#restrictionState.toggleState(this.#restrictionState.states.light, enabled); } /* -------------------------------------------- */ /** * Is this object blocking weather? * @type {boolean} */ get restrictsWeather() { return this.#restrictionState.hasState(this.#restrictionState.states.weather); } set restrictsWeather(enabled) { this.#restrictionState.toggleState(this.#restrictionState.states.weather, enabled); } /* -------------------------------------------- */ /** * Is this occludable object... occludable? * @type {boolean} */ get isOccludable() { return this.occlusionMode > CONST.OCCLUSION_MODES.NONE; } /* -------------------------------------------- */ /** * Debounce assignment of the PCO occluded state to avoid cases like animated token movement which can rapidly * change PCO appearance. * Uses a 50ms debounce threshold. * Objects which are in the hovered state remain occluded until their hovered state ends. * @type {function(occluded: boolean): void} */ debounceSetOcclusion = foundry.utils.debounce(occluded => this.occluded = occluded, 50); /* -------------------------------------------- */ /** @inheritDoc */ updateCanvasTransform() { super.updateCanvasTransform(); this.#updateHoverFadeState(); this.#updateOcclusionState(); } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /** * Update the occlusion state. */ #updateOcclusionState() { const state = this._occlusionState; state.fade = 0; state.radial = 0; state.vision = 0; const M = CONST.OCCLUSION_MODES; switch ( this.occlusionMode ) { case M.FADE: if ( this.occluded ) state.fade = 1; break; case M.RADIAL: state.radial = 1; break; case M.VISION: if ( canvas.masks.occlusion.vision ) state.vision = 1; else if ( this.occluded ) state.fade = 1; break; } const hoverFade = this._hoverFadeState.occlusion; if ( canvas.masks.occlusion.vision ) state.vision = Math.max(state.vision, hoverFade); else state.fade = Math.max(state.fade, hoverFade); } /* -------------------------------------------- */ /** * Update the hover-fade state. */ #updateHoverFadeState() { if ( !this.#hoverFade ) return; const state = this._hoverFadeState; const time = canvas.app.ticker.lastTime; const {delay, duration} = CONFIG.Canvas.hoverFade; if ( state.fading ) { const dt = time - state.fadingTime; if ( dt >= duration ) state.fading = false; } else if ( state.faded !== state.hovered ) { const dt = time - state.hoveredTime; if ( dt >= delay ) { state.faded = state.hovered; if ( dt - delay < duration ) { state.fading = true; state.fadingTime = time; } } } let occlusion = 1; if ( state.fading ) { if ( state.faded !== state.hovered ) { state.faded = state.hovered; state.fadingTime = time - (state.fadingTime + duration - time); } occlusion = CanvasAnimation.easeInOutCosine((time - state.fadingTime) / duration); } state.occlusion = state.faded ? occlusion : 1 - occlusion; } /* -------------------------------------------- */ /* Depth Rendering */ /* -------------------------------------------- */ /** @override */ _shouldRenderDepth() { return !this.#restrictionState.isEmpty && !this.hidden; } /* -------------------------------------------- */ /** * Test whether a specific Token occludes this PCO. * Occlusion is tested against 9 points, the center, the four corners-, and the four cardinal directions * @param {Token} token The Token to test * @param {object} [options] Additional options that affect testing * @param {boolean} [options.corners=true] Test corners of the hit-box in addition to the token center? * @returns {boolean} Is the Token occluded by the PCO? */ testOcclusion(token, {corners=true}={}) { if ( token.document.elevation >= this.elevation ) return false; const {x, y, w, h} = token; let testPoints = [[w / 2, h / 2]]; if ( corners ) { const pad = 2; const cornerPoints = [ [pad, pad], [w / 2, pad], [w - pad, pad], [w - pad, h / 2], [w - pad, h - pad], [w / 2, h - pad], [pad, h - pad], [pad, h / 2] ]; testPoints = testPoints.concat(cornerPoints); } for ( const [tx, ty] of testPoints ) { if ( this.containsCanvasPoint({x: x + tx, y: y + ty}) ) return true; } return false; } /* -------------------------------------------- */ /* Deprecations and Compatibility */ /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get roof() { const msg = `${this.constructor.name}#roof is deprecated in favor of more granular options: ${this.constructor.name}#BlocksLight and ${this.constructor.name}#BlocksWeather`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); return this.restrictsLight && this.restrictsWeather; } /** * @deprecated since v12 * @ignore */ set roof(enabled) { const msg = `${this.constructor.name}#roof is deprecated in favor of more granular options: ${this.constructor.name}#BlocksLight and ${this.constructor.name}#BlocksWeather`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); this.restrictsWeather = enabled; this.restrictsLight = enabled; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ containsPixel(x, y, alphaThreshold=0.75) { const msg = `${this.constructor.name}#containsPixel is deprecated. Use ${this.constructor.name}#containsCanvasPoint instead.`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); return this.containsCanvasPoint({x, y}, alphaThreshold + 1e-6); } /* -------------------------------------------- */ /** * @deprecated since v11 * @ignore */ renderOcclusion(renderer) { const msg = "PrimaryCanvasObject#renderOcclusion is deprecated in favor of PrimaryCanvasObject#renderDepth"; foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13}); this.renderDepthData(renderer); } } return PrimaryOccludableObject; }