Files

422 lines
15 KiB
JavaScript
Raw Permalink Normal View History

2025-01-04 00:34:03 +01:00
/**
* 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;
}
}