This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,455 @@
/**
* A mixin which decorates a DisplayObject with additional properties expected for rendering in the PrimaryCanvasGroup.
* @category - Mixins
* @param {typeof PIXI.DisplayObject} DisplayObject The parent DisplayObject class being mixed
* @returns {typeof PrimaryCanvasObject} A DisplayObject subclass mixed with PrimaryCanvasObject features
* @mixin
*/
function PrimaryCanvasObjectMixin(DisplayObject) {
/**
* A display object rendered in the PrimaryCanvasGroup.
* @param {...*} args The arguments passed to the base class constructor
*/
return class PrimaryCanvasObject extends CanvasTransformMixin(DisplayObject) {
constructor(...args) {
super(...args);
// Activate culling and initialize handlers
this.cullable = true;
this.on("added", this._onAdded);
this.on("removed", this._onRemoved);
}
/**
* An optional reference to the object that owns this PCO.
* This property does not affect the behavior of the PCO itself.
* @type {*}
* @default null
*/
object = null;
/**
* The entry in the quadtree.
* @type {QuadtreeObject|null}
*/
#quadtreeEntry = null;
/**
* Update the quadtree entry?
* @type {boolean}
*/
#quadtreeDirty = false;
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* The elevation of this object.
* @type {number}
*/
get elevation() {
return this.#elevation;
}
set elevation(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#elevation must be a numeric value.");
}
if ( value === this.#elevation ) return;
this.#elevation = value;
if ( this.parent ) {
this.parent.sortDirty = true;
if ( this.shouldRenderDepth ) canvas.masks.depth._elevationDirty = true;
}
}
#elevation = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation within the same layer.
* @type {number}
*/
get sort() {
return this.#sort;
}
set sort(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#sort must be a numeric value.");
}
if ( value === this.#sort ) return;
this.#sort = value;
if ( this.parent ) this.parent.sortDirty = true;
}
#sort = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation of different layers.
* @type {number}
*/
get sortLayer() {
return this.#sortLayer;
}
set sortLayer(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#sortLayer must be a numeric value.");
}
if ( value === this.#sortLayer ) return;
this.#sortLayer = value;
if ( this.parent ) this.parent.sortDirty = true;
}
#sortLayer = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation within the same layer and same sort.
* @type {number}
*/
get zIndex() {
return this._zIndex;
}
set zIndex(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#zIndex must be a numeric value.");
}
if ( value === this._zIndex ) return;
this._zIndex = value;
if ( this.parent ) this.parent.sortDirty = true;
}
/* -------------------------------------------- */
/* PIXI Events */
/* -------------------------------------------- */
/**
* Event fired when this display object is added to a parent.
* @param {PIXI.Container} parent The new parent container.
* @protected
*/
_onAdded(parent) {
if ( parent !== canvas.primary ) {
throw new Error("PrimaryCanvasObject instances may only be direct children of the PrimaryCanvasGroup");
}
}
/* -------------------------------------------- */
/**
* Event fired when this display object is removed from its parent.
* @param {PIXI.Container} parent Parent from which the PCO is removed.
* @protected
*/
_onRemoved(parent) {
this.#updateQuadtree(true);
}
/* -------------------------------------------- */
/* Canvas Transform & Quadtree */
/* -------------------------------------------- */
/** @inheritdoc */
updateCanvasTransform() {
super.updateCanvasTransform();
this.#updateQuadtree();
this.#updateDepth();
}
/* -------------------------------------------- */
/** @inheritdoc */
_onCanvasBoundsUpdate() {
super._onCanvasBoundsUpdate();
this.#quadtreeDirty = true;
}
/* -------------------------------------------- */
/**
* Update the quadtree.
* @param {boolean} [remove=false] Remove the quadtree entry?
*/
#updateQuadtree(remove=false) {
if ( !this.#quadtreeDirty && !remove ) return;
this.#quadtreeDirty = false;
if ( !remove && (this.canvasBounds.width > 0) && (this.canvasBounds.height > 0) ) {
this.#quadtreeEntry ??= {r: this.canvasBounds, t: this};
canvas.primary.quadtree.update(this.#quadtreeEntry);
} else if ( this.#quadtreeEntry ) {
this.#quadtreeEntry = null;
canvas.primary.quadtree.remove(this);
}
}
/* -------------------------------------------- */
/* PCO Properties */
/* -------------------------------------------- */
/**
* Does this object render to the depth buffer?
* @type {boolean}
*/
get shouldRenderDepth() {
return this.#shouldRenderDepth;
}
/** @type {boolean} */
#shouldRenderDepth = false;
/* -------------------------------------------- */
/* Depth Rendering */
/* -------------------------------------------- */
/**
* Flag the depth as dirty if necessary.
*/
#updateDepth() {
const shouldRenderDepth = this._shouldRenderDepth();
if ( this.#shouldRenderDepth === shouldRenderDepth ) return;
this.#shouldRenderDepth = shouldRenderDepth;
canvas.masks.depth._elevationDirty = true;
}
/* -------------------------------------------- */
/**
* Does this object render to the depth buffer?
* @returns {boolean}
* @protected
*/
_shouldRenderDepth() {
return false;
}
/* -------------------------------------------- */
/**
* Render the depth of this object.
* @param {PIXI.Renderer} renderer
*/
renderDepthData(renderer) {}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
renderOcclusion(renderer) {
const msg = "PrimaryCanvasObject#renderOcclusion is deprecated in favor of PrimaryCanvasObject#renderDepthData";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
this.renderDepthData(renderer);
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get document() {
foundry.utils.logCompatibilityWarning("PrimaryCanvasObject#document is deprecated.", {since: 12, until: 14});
if ( !(this.object instanceof PlaceableObject) ) return null;
return this.object.document || null;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
updateBounds() {
const msg = "PrimaryCanvasObject#updateBounds is deprecated and has no effect.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
}
};
}
/**
* A mixin which decorates a DisplayObject with additional properties for canvas transforms and bounds.
* @category - Mixins
* @param {typeof PIXI.Container} DisplayObject The parent DisplayObject class being mixed
* @returns {typeof CanvasTransformMixin} A DisplayObject subclass mixed with CanvasTransformMixin features
* @mixin
*/
function CanvasTransformMixin(DisplayObject) {
return class CanvasTransformMixin extends DisplayObject {
constructor(...args) {
super(...args);
this.on("added", this.#resetCanvasTransformParentID);
this.on("removed", this.#resetCanvasTransformParentID);
}
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* The transform matrix from local space to canvas space.
* @type {PIXI.Matrix}
*/
canvasTransform = new PIXI.Matrix();
/* -------------------------------------------- */
/**
* The update ID of canvas transform matrix.
* @type {number}
* @internal
*/
_canvasTransformID = -1;
/* -------------------------------------------- */
/**
* The update ID of the local transform of this object.
* @type {number}
*/
#canvasTransformLocalID = -1;
/* -------------------------------------------- */
/**
* The update ID of the canvas transform of the parent.
* @type {number}
*/
#canvasTransformParentID = -1;
/* -------------------------------------------- */
/**
* The canvas bounds of this object.
* @type {PIXI.Rectangle}
*/
canvasBounds = new PIXI.Rectangle();
/* -------------------------------------------- */
/**
* The canvas bounds of this object.
* @type {PIXI.Bounds}
* @protected
*/
_canvasBounds = new PIXI.Bounds();
/* -------------------------------------------- */
/**
* The update ID of the canvas bounds.
* Increment to force recalculation.
* @type {number}
* @protected
*/
_canvasBoundsID = 0;
/* -------------------------------------------- */
/**
* Reset the parent ID of the canvas transform.
*/
#resetCanvasTransformParentID() {
this.#canvasTransformParentID = -1;
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**
* Calculate the canvas bounds of this object.
* @protected
*/
_calculateCanvasBounds() {}
/* -------------------------------------------- */
/**
* Recalculate the canvas transform and bounds of this object and its children, if necessary.
*/
updateCanvasTransform() {
this.transform.updateLocalTransform();
// If the local transform or the parent canvas transform has changed,
// recalculate the canvas transform of this object
if ( (this.#canvasTransformLocalID !== this.transform._localID)
|| (this.#canvasTransformParentID !== (this.parent._canvasTransformID ?? 0)) ) {
this.#canvasTransformLocalID = this.transform._localID;
this.#canvasTransformParentID = this.parent._canvasTransformID ?? 0;
this._canvasTransformID++;
this.canvasTransform.copyFrom(this.transform.localTransform);
// Prepend the parent canvas transform matrix (if exists)
if ( this.parent.canvasTransform ) this.canvasTransform.prepend(this.parent.canvasTransform);
this._canvasBoundsID++;
this._onCanvasTransformUpdate();
}
// Recalculate the canvas bounds of this object if necessary
if ( this._canvasBounds.updateID !== this._canvasBoundsID ) {
this._canvasBounds.updateID = this._canvasBoundsID;
this._canvasBounds.clear();
this._calculateCanvasBounds();
// Set the width and height of the canvas bounds rectangle to 0
// if the bounds are empty. PIXI.Bounds#getRectangle does not
// change the rectangle passed to it if the bounds are empty:
// so we need to handle the empty case here.
if ( this._canvasBounds.isEmpty() ) {
this.canvasBounds.x = this.x;
this.canvasBounds.y = this.y;
this.canvasBounds.width = 0;
this.canvasBounds.height = 0;
}
// Update the canvas bounds rectangle
else this._canvasBounds.getRectangle(this.canvasBounds);
this._onCanvasBoundsUpdate();
}
// Recursively update child canvas transforms
const children = this.children;
for ( let i = 0, n = children.length; i < n; i++ ) {
children[i].updateCanvasTransform?.();
}
}
/* -------------------------------------------- */
/**
* Called when the canvas transform changed.
* @protected
*/
_onCanvasTransformUpdate() {}
/* -------------------------------------------- */
/**
* Called when the canvas bounds changed.
* @protected
*/
_onCanvasBoundsUpdate() {}
/* -------------------------------------------- */
/**
* Is the given point in canvas space contained in this object?
* @param {PIXI.IPointData} point The point in canvas space.
* @returns {boolean}
*/
containsCanvasPoint(point) {
return false;
}
};
}

View File

@@ -0,0 +1,93 @@
/**
* A basic PCO which is handling drawings of any shape.
* @extends {PIXI.Graphics}
* @mixes PrimaryCanvasObject
*
* @param {object} [options] A config object
* @param {PIXI.GraphicsGeometry} [options.geometry] A geometry passed to the graphics.
* @param {string|null} [options.name] The name of the PCO.
* @param {*} [options.object] Any object that owns this PCO.
*/
class PrimaryGraphics extends PrimaryCanvasObjectMixin(PIXI.Graphics) {
constructor(options) {
let geometry;
if ( options instanceof PIXI.GraphicsGeometry ) {
geometry = options;
options = {};
} else if ( options instanceof Object ) {
geometry = options.geometry;
} else {
options = {};
}
super(geometry);
this.name = options.name ?? null;
this.object = options.object ?? null;
}
/* -------------------------------------------- */
/**
* A temporary point used by this class.
* @type {PIXI.Point}
*/
static #TEMP_POINT = new PIXI.Point();
/* -------------------------------------------- */
/**
* The dirty ID of the geometry.
* @type {number}
*/
#geometryDirty = -1;
/* -------------------------------------------- */
/**
* Does the geometry contain points?
* @type {boolean}
*/
#geometryContainsPoints = false;
/* -------------------------------------------- */
/** @override */
_calculateCanvasBounds() {
this.finishPoly();
const geometry = this._geometry;
if ( !geometry.graphicsData.length ) return;
const { minX, minY, maxX, maxY } = geometry.bounds;
this._canvasBounds.addFrameMatrix(this.canvasTransform, minX, minY, maxX, maxY);
}
/* -------------------------------------------- */
/** @inheritdoc */
updateCanvasTransform() {
if ( this.#geometryDirty !== this._geometry.dirty ) {
this.#geometryDirty = this._geometry.dirty;
this.#geometryContainsPoints = false;
const graphicsData = this._geometry.graphicsData;
for ( let i = 0; i < graphicsData.length; i++ ) {
const data = graphicsData[i];
if ( data.shape && data.fillStyle.visible ) {
this.#geometryContainsPoints = true;
break;
}
}
this._canvasBoundsID++;
}
super.updateCanvasTransform();
}
/* -------------------------------------------- */
/** @override */
containsCanvasPoint(point) {
if ( !this.#geometryContainsPoints ) return false;
if ( !this.canvasBounds.contains(point.x, point.y) ) return false;
point = this.canvasTransform.applyInverse(point, PrimaryGraphics.#TEMP_POINT);
return this._geometry.containsPoint(point);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,421 @@
/**
* A basic PCO sprite mesh which is handling occlusion and depth.
* @extends {SpriteMesh}
* @mixes PrimaryOccludableObjectMixin
* @mixes PrimaryCanvasObjectMixin
*
* @property {PrimaryBaseSamplerShader} shader The shader bound to this mesh.
*
* @param {object} [options] The constructor options.
* @param {PIXI.Texture} [options.texture] Texture passed to the SpriteMesh.
* @param {typeof PrimaryBaseSamplerShader} [options.shaderClass] The shader class used to render this sprite.
* @param {string|null} [options.name] The name of this sprite.
* @param {*} [options.object] Any object that owns this sprite.
*/
class PrimarySpriteMesh extends PrimaryOccludableObjectMixin(SpriteMesh) {
constructor(options, shaderClass) {
let texture;
if ( options instanceof PIXI.Texture ) {
texture = options;
options = {};
} else if ( options instanceof Object ) {
texture = options.texture;
shaderClass = options.shaderClass;
} else {
options = {};
}
shaderClass ??= PrimaryBaseSamplerShader;
if ( !foundry.utils.isSubclass(shaderClass, PrimaryBaseSamplerShader) ) {
throw new Error(`${shaderClass.name} in not a subclass of PrimaryBaseSamplerShader`);
}
super(texture, shaderClass);
this.name = options.name ?? null;
this.object = options.object ?? null;
}
/* -------------------------------------------- */
/**
* A temporary point used by this class.
* @type {PIXI.Point}
*/
static #TEMP_POINT = new PIXI.Point();
/* -------------------------------------------- */
/**
* The texture alpha data.
* @type {TextureAlphaData|null}
* @protected
*/
_textureAlphaData = null;
/* -------------------------------------------- */
/**
* The texture alpha threshold used for point containment tests.
* If set to a value larger than 0, the texture alpha data is
* extracted from the texture at 25% resolution.
* @type {number}
*/
textureAlphaThreshold = 0;
/* -------------------------------------------- */
/* PIXI Events */
/* -------------------------------------------- */
/** @inheritDoc */
_onTextureUpdate() {
super._onTextureUpdate();
this._textureAlphaData = null;
this._canvasBoundsID++;
}
/* -------------------------------------------- */
/* Helper Methods */
/* -------------------------------------------- */
/** @inheritdoc */
setShaderClass(shaderClass) {
if ( !foundry.utils.isSubclass(shaderClass, PrimaryBaseSamplerShader) ) {
throw new Error(`${shaderClass.name} in not a subclass of PrimaryBaseSamplerShader`);
}
super.setShaderClass(shaderClass);
}
/* -------------------------------------------- */
/**
* An all-in-one helper method: Resizing the PCO according to desired dimensions and options.
* This helper computes the width and height based on the following factors:
*
* - The ratio of texture width and base width.
* - The ratio of texture height and base height.
*
* Additionally, It takes into account the desired fit options:
*
* - (default) "fill" computes the exact width and height ratio.
* - "cover" takes the maximum ratio of width and height and applies it to both.
* - "contain" takes the minimum ratio of width and height and applies it to both.
* - "width" applies the width ratio to both width and height.
* - "height" applies the height ratio to both width and height.
*
* You can also apply optional scaleX and scaleY options to both width and height. The scale is applied after fitting.
*
* **Important**: By using this helper, you don't need to set the height, width, and scale properties of the DisplayObject.
*
* **Note**: This is a helper method. Alternatively, you could assign properties as you would with a PIXI DisplayObject.
*
* @param {number} baseWidth The base width used for computations.
* @param {number} baseHeight The base height used for computations.
* @param {object} [options] The options.
* @param {"fill"|"cover"|"contain"|"width"|"height"} [options.fit="fill"] The fit type.
* @param {number} [options.scaleX=1] The scale on X axis.
* @param {number} [options.scaleY=1] The scale on Y axis.
*/
resize(baseWidth, baseHeight, {fit="fill", scaleX=1, scaleY=1}={}) {
if ( !((baseWidth >= 0) && (baseHeight >= 0)) ) {
throw new Error(`Invalid baseWidth/baseHeight passed to ${this.constructor.name}#resize.`);
}
const {width: textureWidth, height: textureHeight} = this._texture;
let sx;
let sy;
switch ( fit ) {
case "fill":
sx = baseWidth / textureWidth;
sy = baseHeight / textureHeight;
break;
case "cover":
sx = sy = Math.max(baseWidth / textureWidth, baseHeight / textureHeight);
break;
case "contain":
sx = sy = Math.min(baseWidth / textureWidth, baseHeight / textureHeight);
break;
case "width":
sx = sy = baseWidth / textureWidth;
break;
case "height":
sx = sy = baseHeight / textureHeight;
break;
default:
throw new Error(`Invalid fill type passed to ${this.constructor.name}#resize (fit=${fit}).`);
}
sx *= scaleX;
sy *= scaleY;
this.scale.set(sx, sy);
this._width = Math.abs(sx * textureWidth);
this._height = Math.abs(sy * textureHeight);
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/** @inheritdoc */
_updateBatchData() {
super._updateBatchData();
const batchData = this._batchData;
batchData.elevation = this.elevation;
batchData.textureAlphaThreshold = this.textureAlphaThreshold;
batchData.unoccludedAlpha = this.unoccludedAlpha;
batchData.occludedAlpha = this.occludedAlpha;
const occlusionState = this._occlusionState;
batchData.fadeOcclusion = occlusionState.fade;
batchData.radialOcclusion = occlusionState.radial;
batchData.visionOcclusion = occlusionState.vision;
batchData.restrictionState = this._restrictionState;
}
/* -------------------------------------------- */
/** @override */
_calculateCanvasBounds() {
if ( !this._texture ) return;
const {width, height} = this._texture;
let minX = 0;
let minY = 0;
let maxX = width;
let maxY = height;
const alphaData = this._textureAlphaData;
if ( alphaData ) {
const scaleX = width / alphaData.width;
const scaleY = height / alphaData.height;
minX = alphaData.minX * scaleX;
minY = alphaData.minY * scaleY;
maxX = alphaData.maxX * scaleX;
maxY = alphaData.maxY * scaleY;
}
let {x: anchorX, y: anchorY} = this.anchor;
anchorX *= width;
anchorY *= height;
minX -= anchorX;
minY -= anchorY;
maxX -= anchorX;
maxY -= anchorY;
this._canvasBounds.addFrameMatrix(this.canvasTransform, minX, minY, maxX, maxY);
}
/* -------------------------------------------- */
/**
* Is the given point in canvas space contained in this object?
* @param {PIXI.IPointData} point The point in canvas space
* @param {number} [textureAlphaThreshold] The minimum texture alpha required for containment
* @returns {boolean}
*/
containsCanvasPoint(point, textureAlphaThreshold=this.textureAlphaThreshold) {
if ( textureAlphaThreshold > 1 ) return false;
if ( !this.canvasBounds.contains(point.x, point.y) ) return false;
point = this.canvasTransform.applyInverse(point, PrimarySpriteMesh.#TEMP_POINT);
return this.#containsLocalPoint(point, textureAlphaThreshold);
}
/* -------------------------------------------- */
/**
* Is the given point in world space contained in this object?
* @param {PIXI.IPointData} point The point in world space
* @param {number} [textureAlphaThreshold] The minimum texture alpha required for containment
* @returns {boolean}
*/
containsPoint(point, textureAlphaThreshold=this.textureAlphaThreshold) {
if ( textureAlphaThreshold > 1 ) return false;
point = this.worldTransform.applyInverse(point, PrimarySpriteMesh.#TEMP_POINT);
return this.#containsLocalPoint(point, textureAlphaThreshold);
}
/* -------------------------------------------- */
/**
* Is the given point in local space contained in this object?
* @param {PIXI.IPointData} point The point in local space
* @param {number} textureAlphaThreshold The minimum texture alpha required for containment
* @returns {boolean}
*/
#containsLocalPoint(point, textureAlphaThreshold) {
const {width, height} = this._texture;
const {x: anchorX, y: anchorY} = this.anchor;
let {x, y} = point;
x += (width * anchorX);
y += (height * anchorY);
if ( textureAlphaThreshold > 0 ) return this.#getTextureAlpha(x, y) >= textureAlphaThreshold;
return (x >= 0) && (x < width) && (y >= 0) && (y < height);
}
/* -------------------------------------------- */
/**
* Get alpha value of texture at the given texture coordinates.
* @param {number} x The x-coordinate
* @param {number} y The y-coordinate
* @returns {number} The alpha value (0-1)
*/
#getTextureAlpha(x, y) {
if ( !this._texture ) return 0;
if ( !this._textureAlphaData ) {
this._textureAlphaData = TextureLoader.getTextureAlphaData(this._texture, 0.25);
this._canvasBoundsID++;
}
// Transform the texture coordinates
const {width, height} = this._texture;
const alphaData = this._textureAlphaData;
x *= (alphaData.width / width);
y *= (alphaData.height / height);
// First test against the bounding box
const {minX, minY, maxX, maxY} = alphaData;
if ( (x < minX) || (x >= maxX) || (y < minY) || (y >= maxY) ) return 0;
// Get the alpha at the local coordinates
return alphaData.data[((maxX - minX) * ((y | 0) - minY)) + ((x | 0) - minX)] / 255;
}
/* -------------------------------------------- */
/* Rendering Methods */
/* -------------------------------------------- */
/** @override */
renderDepthData(renderer) {
if ( !this.shouldRenderDepth || !this.visible || !this.renderable ) return;
const shader = this._shader;
const blendMode = this.blendMode;
this.blendMode = PIXI.BLEND_MODES.MAX_COLOR;
this._shader = shader.depthShader;
if ( this.cullable ) this._renderWithCulling(renderer);
else this._render(renderer);
this._shader = shader;
this.blendMode = blendMode;
}
/* -------------------------------------------- */
/**
* Render the sprite with ERASE blending.
* Note: The sprite must not have visible/renderable children.
* @param {PIXI.Renderer} renderer The renderer
* @internal
*/
_renderVoid(renderer) {
if ( !this.visible || (this.worldAlpha <= 0) || !this.renderable ) return;
// Delegate to PrimarySpriteMesh#renderVoidAdvanced if the sprite has filter or mask
if ( this._mask || this.filters?.length ) this.#renderVoidAdvanced(renderer);
else {
// Set the blend mode to ERASE before rendering
const originalBlendMode = this.blendMode;
this.blendMode = PIXI.BLEND_MODES.ERASE;
// Render the sprite but not its children
if ( this.cullable ) this._renderWithCulling(renderer);
else this._render(renderer);
// Restore the original blend mode after rendering
this.blendMode = originalBlendMode;
}
}
/* -------------------------------------------- */
/**
* Render the sprite that has a filter or a mask with ERASE blending.
* Note: The sprite must not have visible/renderable children.
* @param {PIXI.Renderer} renderer The renderer
*/
#renderVoidAdvanced(renderer) {
// Same code as in PIXI.Container#renderAdvanced
const filters = this.filters;
const mask = this._mask;
if ( filters ) {
this._enabledFilters ||= [];
this._enabledFilters.length = 0;
for ( let i = 0; i < filters.length; i++ ) {
if ( filters[i].enabled ) this._enabledFilters.push(filters[i]);
}
}
const flush = (filters && this._enabledFilters.length) || (mask && (!mask.isMaskData
|| (mask.enabled && (mask.autoDetect || mask.type !== MASK_TYPES.NONE))));
if ( flush ) renderer.batch.flush();
if ( filters && this._enabledFilters.length ) renderer.filter.push(this, this._enabledFilters);
if ( mask ) renderer.mask.push(this, mask);
// Set the blend mode to ERASE before rendering
let filter;
let originalBlendMode;
const filterState = renderer.filter.defaultFilterStack.at(-1);
if ( filterState.target === this ) {
filter = filterState.filters.at(-1);
originalBlendMode = filter.blendMode;
filter.blendMode = PIXI.BLEND_MODES.ERASE;
} else {
originalBlendMode = this.blendMode;
this.blendMode = PIXI.BLEND_MODES.ERASE;
}
// Same code as in PIXI.Container#renderAdvanced without the part that renders children
if ( this.cullable ) this._renderWithCulling(renderer);
else this._render(renderer);
if ( flush ) renderer.batch.flush();
if ( mask ) renderer.mask.pop(this);
if ( filters && this._enabledFilters.length ) renderer.filter.pop();
// Restore the original blend mode after rendering
if ( filter ) filter.blendMode = originalBlendMode;
else this.blendMode = originalBlendMode;
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getPixelAlpha(x, y) {
const msg = `${this.constructor.name}#getPixelAlpha is deprecated without replacement.`;
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
if ( !this._textureAlphaData ) return null;
if ( !this.canvasBounds.contains(x, y) ) return -1;
const point = PrimarySpriteMesh.#TEMP_POINT.set(x, y);
this.canvasTransform.applyInverse(point, point);
const {width, height} = this._texture;
const {x: anchorX, y: anchorY} = this.anchor;
x = point.x + (width * anchorX);
y = point.y + (height * anchorY);
return this.#getTextureAlpha(x, y) * 255;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
_getAlphaBounds() {
const msg = `${this.constructor.name}#_getAlphaBounds is deprecated without replacement.`;
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
const m = this._textureAlphaData;
const r = this.rotation;
return PIXI.Rectangle.fromRotation(m.minX, m.minY, m.maxX - m.minX, m.maxY - m.minY, r).normalize();
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
_getTextureCoordinate(testX, testY) {
const msg = `${this.constructor.name}#_getTextureCoordinate is deprecated without replacement.`;
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
const point = {x: testX, y: testY};
let {x, y} = this.canvasTransform.applyInverse(point, point);
point.x = ((x / this._texture.width) + this.anchor.x) * this._textureAlphaData.width;
point.y = ((y / this._texture.height) + this.anchor.y) * this._textureAlphaData.height;
return point;
}
}