Initial
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user