Initial
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user