Files
2025-01-04 00:34:03 +01:00

344 lines
10 KiB
JavaScript

/**
* 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;
}