Initial
This commit is contained in:
183
resources/app/client/pixi/core/containers/base-canvas-group.js
Normal file
183
resources/app/client/pixi/core/containers/base-canvas-group.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* A mixin which decorates any container with base canvas common properties.
|
||||
* @category - Mixins
|
||||
* @param {typeof Container} ContainerClass The parent Container class being mixed.
|
||||
* @returns {typeof CanvasGroupMixin} A ContainerClass subclass mixed with CanvasGroupMixin features.
|
||||
*/
|
||||
const CanvasGroupMixin = ContainerClass => {
|
||||
return class CanvasGroup extends ContainerClass {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.sortableChildren = true;
|
||||
this.layers = this._createLayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this canvas group.
|
||||
* @type {string}
|
||||
* @abstract
|
||||
*/
|
||||
static groupName;
|
||||
|
||||
/**
|
||||
* If this canvas group should teardown non-layers children.
|
||||
* @type {boolean}
|
||||
*/
|
||||
static tearDownChildren = true;
|
||||
|
||||
/**
|
||||
* The canonical name of the canvas group is the name of the constructor that is the immediate child of the
|
||||
* defined base class.
|
||||
* @type {string}
|
||||
*/
|
||||
get name() {
|
||||
let cls = Object.getPrototypeOf(this.constructor);
|
||||
let name = this.constructor.name;
|
||||
while ( cls ) {
|
||||
if ( cls !== CanvasGroup ) {
|
||||
name = cls.name;
|
||||
cls = Object.getPrototypeOf(cls);
|
||||
}
|
||||
else break;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name used by hooks to construct their hook string.
|
||||
* Note: You should override this getter if hookName should not return the class constructor name.
|
||||
* @type {string}
|
||||
*/
|
||||
get hookName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping of CanvasLayer classes which belong to this group.
|
||||
* @type {Record<string, CanvasLayer>}
|
||||
*/
|
||||
layers;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create CanvasLayer instances which belong to the canvas group.
|
||||
* @protected
|
||||
*/
|
||||
_createLayers() {
|
||||
const layers = {};
|
||||
for ( let [name, config] of Object.entries(CONFIG.Canvas.layers) ) {
|
||||
if ( config.group !== this.constructor.groupName ) continue;
|
||||
const layer = layers[name] = new config.layerClass();
|
||||
Object.defineProperty(this, name, {value: layer, writable: false});
|
||||
if ( !(name in canvas) ) Object.defineProperty(canvas, name, {value: layer, writable: false});
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An internal reference to a Promise in-progress to draw the canvas group.
|
||||
* @type {Promise<this>}
|
||||
*/
|
||||
#drawing = Promise.resolve(this);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is the group drawn?
|
||||
* @type {boolean}
|
||||
*/
|
||||
#drawn = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the canvas group and all its components.
|
||||
* @param {object} [options={}]
|
||||
* @returns {Promise<this>} A Promise which resolves once the group is fully drawn
|
||||
*/
|
||||
async draw(options={}) {
|
||||
return this.#drawing = this.#drawing.finally(async () => {
|
||||
console.log(`${vtt} | Drawing the ${this.hookName} canvas group`);
|
||||
await this.tearDown();
|
||||
await this._draw(options);
|
||||
Hooks.callAll(`draw${this.hookName}`, this);
|
||||
this.#drawn = true;
|
||||
MouseInteractionManager.emulateMoveEvent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the canvas group and all its component layers.
|
||||
* @param {object} options
|
||||
* @protected
|
||||
*/
|
||||
async _draw(options) {
|
||||
// Draw CanvasLayer instances
|
||||
for ( const layer of Object.values(this.layers) ) {
|
||||
this.addChild(layer);
|
||||
await layer.draw();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tear-Down */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove and destroy all layers from the base canvas.
|
||||
* @param {object} [options={}]
|
||||
* @returns {Promise<this>}
|
||||
*/
|
||||
async tearDown(options={}) {
|
||||
if ( !this.#drawn ) return this;
|
||||
this.#drawn = false;
|
||||
await this._tearDown(options);
|
||||
Hooks.callAll(`tearDown${this.hookName}`, this);
|
||||
MouseInteractionManager.emulateMoveEvent();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and destroy all layers from the base canvas.
|
||||
* @param {object} options
|
||||
* @protected
|
||||
*/
|
||||
async _tearDown(options) {
|
||||
// Remove layers
|
||||
for ( const layer of Object.values(this.layers).reverse() ) {
|
||||
await layer.tearDown();
|
||||
this.removeChild(layer);
|
||||
}
|
||||
|
||||
// Check if we need to handle other children
|
||||
if ( !this.constructor.tearDownChildren ) return;
|
||||
|
||||
// Yes? Then proceed with children cleaning
|
||||
for ( const child of this.removeChildren() ) {
|
||||
if ( child instanceof CachedContainer ) child.clear();
|
||||
else child.destroy({children: true});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
Object.defineProperty(globalThis, "BaseCanvasMixin", {
|
||||
get() {
|
||||
const msg = "BaseCanvasMixin is deprecated in favor of CanvasGroupMixin";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return CanvasGroupMixin;
|
||||
}
|
||||
});
|
||||
333
resources/app/client/pixi/core/containers/cached-container.js
Normal file
333
resources/app/client/pixi/core/containers/cached-container.js
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* A special type of PIXI.Container which draws its contents to a cached RenderTexture.
|
||||
* This is accomplished by overriding the Container#render method to draw to our own special RenderTexture.
|
||||
*/
|
||||
class CachedContainer extends PIXI.Container {
|
||||
/**
|
||||
* Construct a CachedContainer.
|
||||
* @param {PIXI.Sprite|SpriteMesh} [sprite] A specific sprite to bind to this CachedContainer and its renderTexture.
|
||||
*/
|
||||
constructor(sprite) {
|
||||
super();
|
||||
const renderer = canvas.app?.renderer;
|
||||
|
||||
/**
|
||||
* The RenderTexture that is the render destination for the contents of this Container
|
||||
* @type {PIXI.RenderTexture}
|
||||
*/
|
||||
this.#renderTexture = this.createRenderTexture();
|
||||
|
||||
// Bind a sprite to the container
|
||||
if ( sprite ) this.sprite = sprite;
|
||||
|
||||
// Listen for resize events
|
||||
this.#onResize = this.#resize.bind(this, renderer);
|
||||
renderer.on("resize", this.#onResize);
|
||||
}
|
||||
|
||||
/**
|
||||
* The texture configuration to use for this cached container
|
||||
* @type {{multisample: PIXI.MSAA_QUALITY, scaleMode: PIXI.SCALE_MODES, format: PIXI.FORMATS}}
|
||||
* @abstract
|
||||
*/
|
||||
static textureConfiguration = {};
|
||||
|
||||
/**
|
||||
* A bound resize function which fires on the renderer resize event.
|
||||
* @type {function(PIXI.Renderer)}
|
||||
* @private
|
||||
*/
|
||||
#onResize;
|
||||
|
||||
/**
|
||||
* A map of render textures, linked to their render function and an optional RGBA clear color.
|
||||
* @type {Map<PIXI.RenderTexture,{renderFunction: Function, clearColor: number[]}>}
|
||||
* @protected
|
||||
*/
|
||||
_renderPaths = new Map();
|
||||
|
||||
/**
|
||||
* An object which stores a reference to the normal renderer target and source frame.
|
||||
* We track this so we can restore them after rendering our cached texture.
|
||||
* @type {{sourceFrame: PIXI.Rectangle, renderTexture: PIXI.RenderTexture}}
|
||||
* @private
|
||||
*/
|
||||
#backup = {
|
||||
renderTexture: undefined,
|
||||
sourceFrame: canvas.app.renderer.screen.clone()
|
||||
};
|
||||
|
||||
/**
|
||||
* An RGBA array used to define the clear color of the RenderTexture
|
||||
* @type {number[]}
|
||||
*/
|
||||
clearColor = [0, 0, 0, 1];
|
||||
|
||||
/**
|
||||
* Should our Container also be displayed on screen, in addition to being drawn to the cached RenderTexture?
|
||||
* @type {boolean}
|
||||
*/
|
||||
displayed = false;
|
||||
|
||||
/**
|
||||
* If true, the Container is rendered every frame.
|
||||
* If false, the Container is rendered only if {@link CachedContainer#renderDirty} is true.
|
||||
* @type {boolean}
|
||||
*/
|
||||
autoRender = true;
|
||||
|
||||
/**
|
||||
* Does the Container need to be rendered?
|
||||
* Set to false after the Container is rendered.
|
||||
* @type {boolean}
|
||||
*/
|
||||
renderDirty = true;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The primary render texture bound to this cached container.
|
||||
* @type {PIXI.RenderTexture}
|
||||
*/
|
||||
get renderTexture() {
|
||||
return this.#renderTexture;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
#renderTexture;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set the alpha mode of the cached container render texture.
|
||||
* @param {PIXI.ALPHA_MODES} mode
|
||||
*/
|
||||
set alphaMode(mode) {
|
||||
this.#renderTexture.baseTexture.alphaMode = mode;
|
||||
this.#renderTexture.baseTexture.update();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* A PIXI.Sprite or SpriteMesh which is bound to this CachedContainer.
|
||||
* The RenderTexture from this Container is associated with the Sprite which is automatically rendered.
|
||||
* @type {PIXI.Sprite|SpriteMesh}
|
||||
*/
|
||||
get sprite() {
|
||||
return this.#sprite;
|
||||
}
|
||||
|
||||
set sprite(sprite) {
|
||||
if ( sprite instanceof PIXI.Sprite || sprite instanceof SpriteMesh ) {
|
||||
sprite.texture = this.renderTexture;
|
||||
this.#sprite = sprite;
|
||||
}
|
||||
else if ( sprite ) {
|
||||
throw new Error("You may only bind a PIXI.Sprite or a SpriteMesh as the render target for a CachedContainer.");
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
#sprite;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a render texture, provide a render method and an optional clear color.
|
||||
* @param {object} [options={}] Optional parameters.
|
||||
* @param {Function} [options.renderFunction] Render function that will be called to render into the RT.
|
||||
* @param {number[]} [options.clearColor] An optional clear color to clear the RT before rendering into it.
|
||||
* @returns {PIXI.RenderTexture} A reference to the created render texture.
|
||||
*/
|
||||
createRenderTexture({renderFunction, clearColor}={}) {
|
||||
const renderOptions = {};
|
||||
const renderer = canvas.app.renderer;
|
||||
const conf = this.constructor.textureConfiguration;
|
||||
const pm = canvas.performance.mode;
|
||||
|
||||
// Disabling linear filtering by default for low/medium performance mode
|
||||
const defaultScaleMode = (pm > CONST.CANVAS_PERFORMANCE_MODES.MED)
|
||||
? PIXI.SCALE_MODES.LINEAR
|
||||
: PIXI.SCALE_MODES.NEAREST;
|
||||
|
||||
// Creating the render texture
|
||||
const renderTexture = PIXI.RenderTexture.create({
|
||||
width: renderer.screen.width,
|
||||
height: renderer.screen.height,
|
||||
resolution: renderer.resolution,
|
||||
multisample: conf.multisample ?? renderer.multisample,
|
||||
scaleMode: conf.scaleMode ?? defaultScaleMode,
|
||||
format: conf.format ?? PIXI.FORMATS.RGBA
|
||||
});
|
||||
renderOptions.renderFunction = renderFunction; // Binding the render function
|
||||
renderOptions.clearColor = clearColor; // Saving the optional clear color
|
||||
this._renderPaths.set(renderTexture, renderOptions); // Push into the render paths
|
||||
this.renderDirty = true;
|
||||
|
||||
// Return a reference to the render texture
|
||||
return renderTexture;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove a previously created render texture.
|
||||
* @param {PIXI.RenderTexture} renderTexture The render texture to remove.
|
||||
* @param {boolean} [destroy=true] Should the render texture be destroyed?
|
||||
*/
|
||||
removeRenderTexture(renderTexture, destroy=true) {
|
||||
this._renderPaths.delete(renderTexture);
|
||||
if ( destroy ) renderTexture?.destroy(true);
|
||||
this.renderDirty = true;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Clear the cached container, removing its current contents.
|
||||
* @param {boolean} [destroy=true] Tell children that we should destroy texture as well.
|
||||
* @returns {CachedContainer} A reference to the cleared container for chaining.
|
||||
*/
|
||||
clear(destroy=true) {
|
||||
Canvas.clearContainer(this, destroy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
destroy(options) {
|
||||
if ( this.#onResize ) canvas.app.renderer.off("resize", this.#onResize);
|
||||
for ( const [rt] of this._renderPaths ) rt?.destroy(true);
|
||||
this._renderPaths.clear();
|
||||
super.destroy(options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
render(renderer) {
|
||||
if ( !this.renderable ) return; // Skip updating the cached texture
|
||||
if ( this.autoRender || this.renderDirty ) {
|
||||
this.renderDirty = false;
|
||||
this.#bindPrimaryBuffer(renderer); // Bind the primary buffer (RT)
|
||||
super.render(renderer); // Draw into the primary buffer
|
||||
this.#renderSecondary(renderer); // Draw into the secondary buffer(s)
|
||||
this.#bindOriginalBuffer(renderer); // Restore the original buffer
|
||||
}
|
||||
this.#sprite?.render(renderer); // Render the bound sprite
|
||||
if ( this.displayed ) super.render(renderer); // Optionally draw to the screen
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Custom rendering for secondary render textures
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @protected
|
||||
*/
|
||||
#renderSecondary(renderer) {
|
||||
if ( this._renderPaths.size <= 1 ) return;
|
||||
// Bind the render texture and call the custom render method for each render path
|
||||
for ( const [rt, ro] of this._renderPaths ) {
|
||||
if ( !ro.renderFunction ) continue;
|
||||
this.#bind(renderer, rt, ro.clearColor);
|
||||
ro.renderFunction.call(this, renderer);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Bind the primary render texture to the renderer, replacing and saving the original buffer and source frame.
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @private
|
||||
*/
|
||||
#bindPrimaryBuffer(renderer) {
|
||||
|
||||
// Get the RenderTexture to bind
|
||||
const tex = this.renderTexture;
|
||||
const rt = renderer.renderTexture;
|
||||
|
||||
// Backup the current render target
|
||||
this.#backup.renderTexture = rt.current;
|
||||
this.#backup.sourceFrame.copyFrom(rt.sourceFrame);
|
||||
|
||||
// Bind the render texture
|
||||
this.#bind(renderer, tex);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Bind a render texture to this renderer.
|
||||
* Must be called after bindPrimaryBuffer and before bindInitialBuffer.
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @param {PIXI.RenderTexture} tex The texture to bind.
|
||||
* @param {number[]} [clearColor] A custom clear color.
|
||||
* @protected
|
||||
*/
|
||||
#bind(renderer, tex, clearColor) {
|
||||
const rt = renderer.renderTexture;
|
||||
|
||||
// Bind our texture to the renderer
|
||||
renderer.batch.flush();
|
||||
rt.bind(tex, undefined, undefined);
|
||||
rt.clear(clearColor ?? this.clearColor);
|
||||
|
||||
// Enable Filters which are applied to this Container to apply to our cached RenderTexture
|
||||
const fs = renderer.filter.defaultFilterStack;
|
||||
if ( fs.length > 1 ) {
|
||||
fs[fs.length - 1].renderTexture = tex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove the render texture from the Renderer, re-binding the original buffer.
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @private
|
||||
*/
|
||||
#bindOriginalBuffer(renderer) {
|
||||
renderer.batch.flush();
|
||||
|
||||
// Restore Filters to apply to the original RenderTexture
|
||||
const fs = renderer.filter.defaultFilterStack;
|
||||
if ( fs.length > 1 ) {
|
||||
fs[fs.length - 1].renderTexture = this.#backup.renderTexture;
|
||||
}
|
||||
|
||||
// Re-bind the original RenderTexture to the renderer
|
||||
renderer.renderTexture.bind(this.#backup.renderTexture, this.#backup.sourceFrame, undefined);
|
||||
this.#backup.renderTexture = undefined;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Resize bound render texture(s) when the dimensions or resolution of the Renderer have changed.
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @private
|
||||
*/
|
||||
#resize(renderer) {
|
||||
for ( const [rt] of this._renderPaths ) CachedContainer.resizeRenderTexture(renderer, rt);
|
||||
if ( this.#sprite ) this.#sprite._boundsID++; // Inform PIXI that bounds need to be recomputed for this sprite mesh
|
||||
this.renderDirty = true;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Resize a render texture passed as a parameter with the renderer.
|
||||
* @param {PIXI.Renderer} renderer The active canvas renderer.
|
||||
* @param {PIXI.RenderTexture} rt The render texture to resize.
|
||||
*/
|
||||
static resizeRenderTexture(renderer, rt) {
|
||||
const screen = renderer?.screen;
|
||||
if ( !rt || !screen ) return;
|
||||
if ( rt.baseTexture.resolution !== renderer.resolution ) rt.baseTexture.resolution = renderer.resolution;
|
||||
if ( (rt.width !== screen.width) || (rt.height !== screen.height) ) rt.resize(screen.width, screen.height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Augment any PIXI.DisplayObject to assume bounds that are always aligned with the full visible screen.
|
||||
* The bounds of this container do not depend on its children but always fill the entire canvas.
|
||||
* @param {typeof PIXI.DisplayObject} Base Any PIXI DisplayObject subclass
|
||||
* @returns {typeof FullCanvasObject} The decorated subclass with full canvas bounds
|
||||
*/
|
||||
function FullCanvasObjectMixin(Base) {
|
||||
return class FullCanvasObject extends Base {
|
||||
/** @override */
|
||||
calculateBounds() {
|
||||
const bounds = this._bounds;
|
||||
const { x, y, width, height } = canvas.dimensions.rect;
|
||||
bounds.clear();
|
||||
bounds.addFrame(this.transform, x, y, x + width, y + height);
|
||||
bounds.updateID = this._boundsID;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
class FullCanvasContainer extends FullCanvasObjectMixin(PIXI.Container) {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const msg = "You are using the FullCanvasContainer class which has been deprecated in favor of a more flexible "
|
||||
+ "FullCanvasObjectMixin which can augment any PIXI.DisplayObject subclass.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
|
||||
}
|
||||
}
|
||||
118
resources/app/client/pixi/core/containers/point-source-mesh.js
Normal file
118
resources/app/client/pixi/core/containers/point-source-mesh.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Extension of a PIXI.Mesh, with the capabilities to provide a snapshot of the framebuffer.
|
||||
* @extends PIXI.Mesh
|
||||
*/
|
||||
class PointSourceMesh extends PIXI.Mesh {
|
||||
/**
|
||||
* To store the previous blend mode of the last renderer PointSourceMesh.
|
||||
* @type {PIXI.BLEND_MODES}
|
||||
* @protected
|
||||
*/
|
||||
static _priorBlendMode;
|
||||
|
||||
/**
|
||||
* The current texture used by the mesh.
|
||||
* @type {PIXI.Texture}
|
||||
* @protected
|
||||
*/
|
||||
static _currentTexture;
|
||||
|
||||
/**
|
||||
* The transform world ID of the bounds.
|
||||
* @type {number}
|
||||
*/
|
||||
_worldID = -1;
|
||||
|
||||
/**
|
||||
* The geometry update ID of the bounds.
|
||||
* @type {number}
|
||||
*/
|
||||
_updateID = -1;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* PointSourceMesh Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get geometry() {
|
||||
return super.geometry;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
set geometry(value) {
|
||||
if ( this._geometry !== value ) this._updateID = -1;
|
||||
super.geometry = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* PointSourceMesh Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
addChild() {
|
||||
throw new Error("You can't add children to a PointSourceMesh.");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
addChildAt() {
|
||||
throw new Error("You can't add children to a PointSourceMesh.");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_render(renderer) {
|
||||
if ( this.uniforms.framebufferTexture !== undefined ) {
|
||||
if ( canvas.blur.enabled ) {
|
||||
// We need to use the snapshot only if blend mode is changing
|
||||
const requireUpdate = (this.state.blendMode !== PointSourceMesh._priorBlendMode)
|
||||
&& (PointSourceMesh._priorBlendMode !== undefined);
|
||||
if ( requireUpdate ) PointSourceMesh._currentTexture = canvas.snapshot.getFramebufferTexture(renderer);
|
||||
PointSourceMesh._priorBlendMode = this.state.blendMode;
|
||||
}
|
||||
this.uniforms.framebufferTexture = PointSourceMesh._currentTexture;
|
||||
}
|
||||
super._render(renderer);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
calculateBounds() {
|
||||
const {transform, geometry} = this;
|
||||
|
||||
// Checking bounds id to update only when it is necessary
|
||||
if ( this._worldID !== transform._worldID
|
||||
|| this._updateID !== geometry.buffers[0]._updateID ) {
|
||||
|
||||
this._worldID = transform._worldID;
|
||||
this._updateID = geometry.buffers[0]._updateID;
|
||||
|
||||
const {x, y, width, height} = this.geometry.bounds;
|
||||
this._bounds.clear();
|
||||
this._bounds.addFrame(transform, x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
this._bounds.updateID = this._boundsID;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_calculateBounds() {
|
||||
this.calculateBounds();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The local bounds need to be drawn from the underlying geometry.
|
||||
* @override
|
||||
*/
|
||||
getLocalBounds(rect) {
|
||||
rect ??= this._localBoundsRect ??= new PIXI.Rectangle();
|
||||
return this.geometry.bounds.copyTo(rect);
|
||||
}
|
||||
}
|
||||
125
resources/app/client/pixi/core/containers/quad-mesh.js
Normal file
125
resources/app/client/pixi/core/containers/quad-mesh.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* A basic rectangular mesh with a shader only. Does not natively handle textures (but a bound shader can).
|
||||
* Bounds calculations are simplified and the geometry does not need to handle texture coords.
|
||||
* @param {AbstractBaseShader} shaderClass The shader class to use.
|
||||
*/
|
||||
class QuadMesh extends PIXI.Container {
|
||||
constructor(shaderClass) {
|
||||
super();
|
||||
// Assign shader, state and properties
|
||||
if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
|
||||
throw new Error("QuadMesh shader class must inherit from AbstractBaseShader.");
|
||||
}
|
||||
this.#shader = shaderClass.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Geometry bound to this QuadMesh.
|
||||
* @type {PIXI.Geometry}
|
||||
*/
|
||||
#geometry = new PIXI.Geometry()
|
||||
.addAttribute("aVertexPosition", [0, 0, 1, 0, 1, 1, 0, 1], 2)
|
||||
.addIndex([0, 1, 2, 0, 2, 3]);
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The shader bound to this mesh.
|
||||
* @type {AbstractBaseShader}
|
||||
*/
|
||||
get shader() {
|
||||
return this.#shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {AbstractBaseShader}
|
||||
*/
|
||||
#shader;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Assigned blend mode to this mesh.
|
||||
* @type {PIXI.BLEND_MODES}
|
||||
*/
|
||||
get blendMode() {
|
||||
return this.#state.blendMode;
|
||||
}
|
||||
|
||||
set blendMode(value) {
|
||||
this.#state.blendMode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* State bound to this QuadMesh.
|
||||
* @type {PIXI.State}
|
||||
*/
|
||||
#state = PIXI.State.for2d();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize shader based on the shader class type.
|
||||
* @param {class} shaderClass Shader class used. Must inherit from AbstractBaseShader.
|
||||
*/
|
||||
setShaderClass(shaderClass) {
|
||||
// Escape conditions
|
||||
if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
|
||||
throw new Error("QuadMesh shader class must inherit from AbstractBaseShader.");
|
||||
}
|
||||
if ( this.#shader.constructor === shaderClass ) return;
|
||||
|
||||
// Create shader program
|
||||
this.#shader = shaderClass.create();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_render(renderer) {
|
||||
this.#shader._preRender(this, renderer);
|
||||
this.#shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true);
|
||||
|
||||
// Flush batch renderer
|
||||
renderer.batch.flush();
|
||||
|
||||
// Set state
|
||||
renderer.state.set(this.#state);
|
||||
|
||||
// Bind shader and geometry
|
||||
renderer.shader.bind(this.#shader);
|
||||
renderer.geometry.bind(this.#geometry, this.#shader);
|
||||
|
||||
// Draw the geometry
|
||||
renderer.geometry.draw(PIXI.DRAW_MODES.TRIANGLES);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_calculateBounds() {
|
||||
this._bounds.addFrame(this.transform, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Tests if a point is inside this QuadMesh.
|
||||
* @param {PIXI.IPointData} point
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsPoint(point) {
|
||||
return this.getBounds().contains(point.x, point.y);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
destroy(options) {
|
||||
super.destroy(options);
|
||||
this.#geometry.dispose();
|
||||
this.#geometry = null;
|
||||
this.#shader = null;
|
||||
this.#state = null;
|
||||
}
|
||||
}
|
||||
319
resources/app/client/pixi/core/containers/quadtree.js
Normal file
319
resources/app/client/pixi/core/containers/quadtree.js
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* @typedef {object} QuadtreeObject
|
||||
* @property {Rectangle} r
|
||||
* @property {*} t
|
||||
* @property {Set<Quadtree>} [n]
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Quadtree implementation that supports collision detection for rectangles.
|
||||
*
|
||||
* @param {Rectangle} bounds The outer bounds of the region
|
||||
* @param {object} [options] Additional options which configure the Quadtree
|
||||
* @param {number} [options.maxObjects=20] The maximum number of objects per node
|
||||
* @param {number} [options.maxDepth=4] The maximum number of levels within the root Quadtree
|
||||
* @param {number} [options._depth=0] The depth level of the sub-tree. For internal use
|
||||
* @param {number} [options._root] The root of the quadtree. For internal use
|
||||
*/
|
||||
class Quadtree {
|
||||
constructor(bounds, {maxObjects=20, maxDepth=4, _depth=0, _root}={}) {
|
||||
|
||||
/**
|
||||
* The bounding rectangle of the region
|
||||
* @type {PIXI.Rectangle}
|
||||
*/
|
||||
this.bounds = new PIXI.Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
|
||||
|
||||
/**
|
||||
* The maximum number of objects allowed within this node before it must split
|
||||
* @type {number}
|
||||
*/
|
||||
this.maxObjects = maxObjects;
|
||||
|
||||
/**
|
||||
* The maximum number of levels that the base quadtree is allowed
|
||||
* @type {number}
|
||||
*/
|
||||
this.maxDepth = maxDepth;
|
||||
|
||||
/**
|
||||
* The depth of this node within the root Quadtree
|
||||
* @type {number}
|
||||
*/
|
||||
this.depth = _depth;
|
||||
|
||||
/**
|
||||
* The objects contained at this level of the tree
|
||||
* @type {QuadtreeObject[]}
|
||||
*/
|
||||
this.objects = [];
|
||||
|
||||
/**
|
||||
* Children of this node
|
||||
* @type {Quadtree[]}
|
||||
*/
|
||||
this.nodes = [];
|
||||
|
||||
/**
|
||||
* The root Quadtree
|
||||
* @type {Quadtree}
|
||||
*/
|
||||
this.root = _root || this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constant that enumerates the index order of the quadtree nodes from top-left to bottom-right.
|
||||
* @enum {number}
|
||||
*/
|
||||
static INDICES = {tl: 0, tr: 1, bl: 2, br: 3};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return an array of all the objects in the Quadtree (recursive)
|
||||
* @returns {QuadtreeObject[]}
|
||||
*/
|
||||
get all() {
|
||||
if ( this.nodes.length ) {
|
||||
return this.nodes.reduce((arr, n) => arr.concat(n.all), []);
|
||||
}
|
||||
return this.objects;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tree Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Split this node into 4 sub-nodes.
|
||||
* @returns {Quadtree} The split Quadtree
|
||||
*/
|
||||
split() {
|
||||
const b = this.bounds;
|
||||
const w = b.width / 2;
|
||||
const h = b.height / 2;
|
||||
const options = {
|
||||
maxObjects: this.maxObjects,
|
||||
maxDepth: this.maxDepth,
|
||||
_depth: this.depth + 1,
|
||||
_root: this.root
|
||||
};
|
||||
|
||||
// Create child quadrants
|
||||
this.nodes[Quadtree.INDICES.tl] = new Quadtree(new PIXI.Rectangle(b.x, b.y, w, h), options);
|
||||
this.nodes[Quadtree.INDICES.tr] = new Quadtree(new PIXI.Rectangle(b.x+w, b.y, w, h), options);
|
||||
this.nodes[Quadtree.INDICES.bl] = new Quadtree(new PIXI.Rectangle(b.x, b.y+h, w, h), options);
|
||||
this.nodes[Quadtree.INDICES.br] = new Quadtree(new PIXI.Rectangle(b.x+w, b.y+h, w, h), options);
|
||||
|
||||
// Assign current objects to child nodes
|
||||
for ( let o of this.objects ) {
|
||||
o.n.delete(this);
|
||||
this.insert(o);
|
||||
}
|
||||
this.objects = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Object Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Clear the quadtree of all existing contents
|
||||
* @returns {Quadtree} The cleared Quadtree
|
||||
*/
|
||||
clear() {
|
||||
this.objects = [];
|
||||
for ( let n of this.nodes ) {
|
||||
n.clear();
|
||||
}
|
||||
this.nodes = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a rectangle object to the tree
|
||||
* @param {QuadtreeObject} obj The object being inserted
|
||||
* @returns {Quadtree[]} The Quadtree nodes the object was added to.
|
||||
*/
|
||||
insert(obj) {
|
||||
obj.n = obj.n || new Set();
|
||||
|
||||
// If we will exceeded the maximum objects we need to split
|
||||
if ( (this.objects.length === this.maxObjects - 1) && (this.depth < this.maxDepth) ) {
|
||||
if ( !this.nodes.length ) this.split();
|
||||
}
|
||||
|
||||
// If this node has children, recursively insert
|
||||
if ( this.nodes.length ) {
|
||||
let nodes = this.getChildNodes(obj.r);
|
||||
return nodes.reduce((arr, n) => arr.concat(n.insert(obj)), []);
|
||||
}
|
||||
|
||||
// Otherwise store the object here
|
||||
obj.n.add(this);
|
||||
this.objects.push(obj);
|
||||
return [this];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove an object from the quadtree
|
||||
* @param {*} target The quadtree target being removed
|
||||
* @returns {Quadtree} The Quadtree for method chaining
|
||||
*/
|
||||
remove(target) {
|
||||
this.objects.findSplice(o => o.t === target);
|
||||
for ( let n of this.nodes ) {
|
||||
n.remove(target);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove an existing object from the quadtree and re-insert it with a new position
|
||||
* @param {QuadtreeObject} obj The object being inserted
|
||||
* @returns {Quadtree[]} The Quadtree nodes the object was added to
|
||||
*/
|
||||
update(obj) {
|
||||
this.remove(obj.t);
|
||||
return this.insert(obj);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Target Identification */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get all the objects which could collide with the provided rectangle
|
||||
* @param {Rectangle} rect The normalized target rectangle
|
||||
* @param {object} [options] Options affecting the collision test.
|
||||
* @param {Function} [options.collisionTest] Function to further refine objects to return
|
||||
* after a potential collision is found. Parameters are the object and rect, and the
|
||||
* function should return true if the object should be added to the result set.
|
||||
* @param {Set} [options._s] The existing result set, for internal use.
|
||||
* @returns {Set} The objects in the Quadtree which represent potential collisions
|
||||
*/
|
||||
getObjects(rect, { collisionTest, _s } = {}) {
|
||||
const objects = _s || new Set();
|
||||
|
||||
// Recursively retrieve objects from child nodes
|
||||
if ( this.nodes.length ) {
|
||||
const nodes = this.getChildNodes(rect);
|
||||
for ( let n of nodes ) {
|
||||
n.getObjects(rect, {collisionTest, _s: objects});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, retrieve from this node
|
||||
else {
|
||||
for ( let o of this.objects) {
|
||||
if ( rect.overlaps(o.r) && (!collisionTest || collisionTest(o, rect)) ) objects.add(o.t);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the result set
|
||||
return objects;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Obtain the leaf nodes to which a target rectangle belongs.
|
||||
* This traverses the quadtree recursively obtaining the final nodes which have no children.
|
||||
* @param {Rectangle} rect The target rectangle.
|
||||
* @returns {Quadtree[]} The Quadtree nodes to which the target rectangle belongs
|
||||
*/
|
||||
getLeafNodes(rect) {
|
||||
if ( !this.nodes.length ) return [this];
|
||||
const nodes = this.getChildNodes(rect);
|
||||
return nodes.reduce((arr, n) => arr.concat(n.getLeafNodes(rect)), []);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Obtain the child nodes within the current node which a rectangle belongs to.
|
||||
* Note that this function is not recursive, it only returns nodes at the current or child level.
|
||||
* @param {Rectangle} rect The target rectangle.
|
||||
* @returns {Quadtree[]} The Quadtree nodes to which the target rectangle belongs
|
||||
*/
|
||||
getChildNodes(rect) {
|
||||
|
||||
// If this node has no children, use it
|
||||
if ( !this.nodes.length ) return [this];
|
||||
|
||||
// Prepare data
|
||||
const nodes = [];
|
||||
const hx = this.bounds.x + (this.bounds.width / 2);
|
||||
const hy = this.bounds.y + (this.bounds.height / 2);
|
||||
|
||||
// Determine orientation relative to the node
|
||||
const startTop = rect.y <= hy;
|
||||
const startLeft = rect.x <= hx;
|
||||
const endBottom = (rect.y + rect.height) > hy;
|
||||
const endRight = (rect.x + rect.width) > hx;
|
||||
|
||||
// Top-left
|
||||
if ( startLeft && startTop ) nodes.push(this.nodes[Quadtree.INDICES.tl]);
|
||||
|
||||
// Top-right
|
||||
if ( endRight && startTop ) nodes.push(this.nodes[Quadtree.INDICES.tr]);
|
||||
|
||||
// Bottom-left
|
||||
if ( startLeft && endBottom ) nodes.push(this.nodes[Quadtree.INDICES.bl]);
|
||||
|
||||
// Bottom-right
|
||||
if ( endRight && endBottom ) nodes.push(this.nodes[Quadtree.INDICES.br]);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Identify all nodes which are adjacent to this one within the parent Quadtree.
|
||||
* @returns {Quadtree[]}
|
||||
*/
|
||||
getAdjacentNodes() {
|
||||
const bounds = this.bounds.clone().pad(1);
|
||||
return this.root.getLeafNodes(bounds);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Visualize the nodes and objects in the quadtree
|
||||
* @param {boolean} [objects] Visualize the rectangular bounds of objects in the Quadtree. Default is false.
|
||||
* @private
|
||||
*/
|
||||
visualize({objects=false}={}) {
|
||||
const debug = canvas.controls.debug;
|
||||
if ( this.depth === 0 ) debug.clear().endFill();
|
||||
debug.lineStyle(2, 0x00FF00, 0.5).drawRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height);
|
||||
if ( objects ) {
|
||||
for ( let o of this.objects ) {
|
||||
debug.lineStyle(2, 0xFF0000, 0.5).drawRect(o.r.x, o.r.y, Math.max(o.r.width, 1), Math.max(o.r.height, 1));
|
||||
}
|
||||
}
|
||||
for ( let n of this.nodes ) {
|
||||
n.visualize({objects});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A subclass of Quadtree specifically intended for classifying the location of objects on the game canvas.
|
||||
*/
|
||||
class CanvasQuadtree extends Quadtree {
|
||||
constructor(options={}) {
|
||||
super({}, options);
|
||||
Object.defineProperty(this, "bounds", {get: () => canvas.dimensions.rect});
|
||||
}
|
||||
}
|
||||
820
resources/app/client/pixi/core/containers/sprite-mesh.js
Normal file
820
resources/app/client/pixi/core/containers/sprite-mesh.js
Normal file
@@ -0,0 +1,820 @@
|
||||
/**
|
||||
* An extension of PIXI.Mesh which emulate a PIXI.Sprite with a specific shader.
|
||||
* @param {PIXI.Texture} [texture=PIXI.Texture.EMPTY] Texture bound to this sprite mesh.
|
||||
* @param {typeof BaseSamplerShader} [shaderClass=BaseSamplerShader] Shader class used by this sprite mesh.
|
||||
*/
|
||||
class SpriteMesh extends PIXI.Container {
|
||||
constructor(texture, shaderClass=BaseSamplerShader) {
|
||||
super();
|
||||
// Create shader program
|
||||
if ( !foundry.utils.isSubclass(shaderClass, BaseSamplerShader) ) {
|
||||
throw new Error("SpriteMesh shader class must be a subclass of BaseSamplerShader.");
|
||||
}
|
||||
this._shader = shaderClass.create();
|
||||
|
||||
// Initialize other data to emulate sprite
|
||||
this.vertexData = this.#geometry.buffers[0].data;
|
||||
this.uvs = this.#geometry.buffers[1].data;
|
||||
this.indices = this.#geometry.indexBuffer.data;
|
||||
|
||||
this._texture = null;
|
||||
this._anchor = new PIXI.ObservablePoint(
|
||||
this._onAnchorUpdate,
|
||||
this,
|
||||
(texture ? texture.defaultAnchor.x : 0),
|
||||
(texture ? texture.defaultAnchor.y : 0)
|
||||
);
|
||||
this.texture = texture;
|
||||
this.isSprite = true;
|
||||
|
||||
// Assigning some batch data that will not change during the life of this sprite mesh
|
||||
this._batchData.vertexData = this.vertexData;
|
||||
this._batchData.indices = this.indices;
|
||||
this._batchData.uvs = this.uvs;
|
||||
this._batchData.object = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A temporary reusable rect.
|
||||
* @type {PIXI.Rectangle}
|
||||
*/
|
||||
static #TEMP_RECT = new PIXI.Rectangle();
|
||||
|
||||
/**
|
||||
* A temporary reusable point.
|
||||
* @type {PIXI.Point}
|
||||
*/
|
||||
static #TEMP_POINT = new PIXI.Point();
|
||||
|
||||
/**
|
||||
* Geometry bound to this SpriteMesh.
|
||||
* @type {PIXI.Geometry}
|
||||
*/
|
||||
#geometry = new PIXI.Geometry()
|
||||
.addAttribute("aVertexPosition", new PIXI.Buffer(new Float32Array(8), false), 2)
|
||||
.addAttribute("aTextureCoord", new PIXI.Buffer(new Float32Array(8), true), 2)
|
||||
.addIndex([0, 1, 2, 0, 2, 3]);
|
||||
|
||||
/**
|
||||
* Snapshot of some parameters of this display object to render in batched mode.
|
||||
* @type {{_tintRGB: number, _texture: PIXI.Texture, indices: number[],
|
||||
* uvs: number[], blendMode: PIXI.BLEND_MODES, vertexData: number[], worldAlpha: number}}
|
||||
* @protected
|
||||
*/
|
||||
_batchData = {
|
||||
_texture: undefined,
|
||||
vertexData: undefined,
|
||||
indices: undefined,
|
||||
uvs: undefined,
|
||||
worldAlpha: undefined,
|
||||
_tintRGB: undefined,
|
||||
blendMode: undefined,
|
||||
object: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* The indices of the geometry.
|
||||
* @type {Uint16Array}
|
||||
*/
|
||||
indices;
|
||||
|
||||
/**
|
||||
* The width of the sprite (this is initially set by the texture).
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_width = 0;
|
||||
|
||||
/**
|
||||
* The height of the sprite (this is initially set by the texture)
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_height = 0;
|
||||
|
||||
/**
|
||||
* The texture that the sprite is using.
|
||||
* @type {PIXI.Texture}
|
||||
* @protected
|
||||
*/
|
||||
_texture;
|
||||
|
||||
/**
|
||||
* The texture ID.
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_textureID = -1;
|
||||
|
||||
/**
|
||||
* Cached tint value so we can tell when the tint is changed.
|
||||
* @type {[red: number, green: number, blue: number, alpha: number]}
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
_cachedTint = [1, 1, 1, 1];
|
||||
|
||||
/**
|
||||
* The texture trimmed ID.
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_textureTrimmedID = -1;
|
||||
|
||||
/**
|
||||
* This is used to store the uvs data of the sprite, assigned at the same time
|
||||
* as the vertexData in calculateVertices().
|
||||
* @type {Float32Array}
|
||||
* @protected
|
||||
*/
|
||||
uvs;
|
||||
|
||||
/**
|
||||
* The anchor point defines the normalized coordinates
|
||||
* in the texture that map to the position of this
|
||||
* sprite.
|
||||
*
|
||||
* By default, this is `(0,0)` (or `texture.defaultAnchor`
|
||||
* if you have modified that), which means the position
|
||||
* `(x,y)` of this `Sprite` will be the top-left corner.
|
||||
*
|
||||
* Note: Updating `texture.defaultAnchor` after
|
||||
* constructing a `Sprite` does _not_ update its anchor.
|
||||
*
|
||||
* {@link https://docs.cocos2d-x.org/cocos2d-x/en/sprites/manipulation.html}
|
||||
* @type {PIXI.ObservablePoint}
|
||||
* @protected
|
||||
*/
|
||||
_anchor;
|
||||
|
||||
/**
|
||||
* This is used to store the vertex data of the sprite (basically a quad).
|
||||
* @type {Float32Array}
|
||||
* @protected
|
||||
*/
|
||||
vertexData;
|
||||
|
||||
/**
|
||||
* This is used to calculate the bounds of the object IF it is a trimmed sprite.
|
||||
* @type {Float32Array|null}
|
||||
* @protected
|
||||
*/
|
||||
vertexTrimmedData = null;
|
||||
|
||||
/**
|
||||
* The transform ID.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
_transformID = -1;
|
||||
|
||||
/**
|
||||
* The transform ID.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
_transformTrimmedID = -1;
|
||||
|
||||
/**
|
||||
* The tint applied to the sprite. This is a hex value. A value of 0xFFFFFF will remove any tint effect.
|
||||
* @type {PIXI.Color}
|
||||
* @protected
|
||||
*/
|
||||
_tintColor = new PIXI.Color(0xFFFFFF);
|
||||
|
||||
/**
|
||||
* The tint applied to the sprite. This is a RGB value. A value of 0xFFFFFF will remove any tint effect.
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_tintRGB = 0xFFFFFF;
|
||||
|
||||
/**
|
||||
* An instance of a texture uvs used for padded SpriteMesh.
|
||||
* Instanced only when padding becomes non-zero.
|
||||
* @type {PIXI.TextureUvs|null}
|
||||
* @protected
|
||||
*/
|
||||
_textureUvs = null;
|
||||
|
||||
/**
|
||||
* Used to track a tint or alpha change to execute a recomputation of _cachedTint.
|
||||
* @type {boolean}
|
||||
* @protected
|
||||
*/
|
||||
_tintAlphaDirty = true;
|
||||
|
||||
/**
|
||||
* The PIXI.State of this SpriteMesh.
|
||||
* @type {PIXI.State}
|
||||
*/
|
||||
#state = PIXI.State.for2d();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The shader bound to this mesh.
|
||||
* @type {BaseSamplerShader}
|
||||
*/
|
||||
get shader() {
|
||||
return this._shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* The shader bound to this mesh.
|
||||
* @type {BaseSamplerShader}
|
||||
* @protected
|
||||
*/
|
||||
_shader;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The x padding in pixels (must be a non-negative value.)
|
||||
* @type {number}
|
||||
*/
|
||||
get paddingX() {
|
||||
return this._paddingX;
|
||||
}
|
||||
|
||||
set paddingX(value) {
|
||||
if ( value < 0 ) throw new Error("The padding must be a non-negative value.");
|
||||
if ( this._paddingX === value ) return;
|
||||
this._paddingX = value;
|
||||
this._textureID = -1;
|
||||
this._textureTrimmedID = -1;
|
||||
this._textureUvs ??= new PIXI.TextureUvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* They y padding in pixels (must be a non-negative value.)
|
||||
* @type {number}
|
||||
*/
|
||||
get paddingY() {
|
||||
return this._paddingY;
|
||||
}
|
||||
|
||||
set paddingY(value) {
|
||||
if ( value < 0 ) throw new Error("The padding must be a non-negative value.");
|
||||
if ( this._paddingY === value ) return;
|
||||
this._paddingY = value;
|
||||
this._textureID = -1;
|
||||
this._textureTrimmedID = -1;
|
||||
this._textureUvs ??= new PIXI.TextureUvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum x/y padding in pixels (must be a non-negative value.)
|
||||
* @type {number}
|
||||
*/
|
||||
get padding() {
|
||||
return Math.max(this._paddingX, this._paddingY);
|
||||
}
|
||||
|
||||
set padding(value) {
|
||||
if ( value < 0 ) throw new Error("The padding must be a non-negative value.");
|
||||
this.paddingX = this.paddingY = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_paddingX = 0;
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
_paddingY = 0;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The blend mode applied to the SpriteMesh.
|
||||
* @type {PIXI.BLEND_MODES}
|
||||
* @defaultValue PIXI.BLEND_MODES.NORMAL
|
||||
*/
|
||||
set blendMode(value) {
|
||||
this.#state.blendMode = value;
|
||||
}
|
||||
|
||||
get blendMode() {
|
||||
return this.#state.blendMode;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* If true PixiJS will Math.round() x/y values when rendering, stopping pixel interpolation.
|
||||
* Advantages can include sharper image quality (like text) and faster rendering on canvas.
|
||||
* The main disadvantage is movement of objects may appear less smooth.
|
||||
* To set the global default, change PIXI.settings.ROUND_PIXELS
|
||||
* @defaultValue PIXI.settings.ROUND_PIXELS
|
||||
*/
|
||||
set roundPixels(value) {
|
||||
if ( this.#roundPixels !== value ) this._transformID = -1;
|
||||
this.#roundPixels = value;
|
||||
}
|
||||
|
||||
get roundPixels() {
|
||||
return this.#roundPixels;
|
||||
}
|
||||
|
||||
#roundPixels = PIXI.settings.ROUND_PIXELS;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Used to force an alpha mode on this sprite mesh.
|
||||
* If this property is non null, this value will replace the texture alphaMode when computing color channels.
|
||||
* Affects how tint, worldAlpha and alpha are computed each others.
|
||||
* @type {PIXI.ALPHA_MODES}
|
||||
*/
|
||||
get alphaMode() {
|
||||
return this.#alphaMode ?? this._texture.baseTexture.alphaMode;
|
||||
}
|
||||
|
||||
set alphaMode(mode) {
|
||||
if ( this.#alphaMode === mode ) return;
|
||||
this.#alphaMode = mode;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
|
||||
#alphaMode = null;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Returns the SpriteMesh associated batch plugin. By default the returned plugin is that of the associated shader.
|
||||
* If a plugin is forced, it will returns the forced plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
get pluginName() {
|
||||
return this.#pluginName ?? this._shader.pluginName;
|
||||
}
|
||||
|
||||
set pluginName(name) {
|
||||
this.#pluginName = name;
|
||||
}
|
||||
|
||||
#pluginName = null;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get width() {
|
||||
return Math.abs(this.scale.x) * this._texture.orig.width;
|
||||
}
|
||||
|
||||
set width(width) {
|
||||
const s = Math.sign(this.scale.x) || 1;
|
||||
this.scale.x = s * width / this._texture.orig.width;
|
||||
this._width = width;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get height() {
|
||||
return Math.abs(this.scale.y) * this._texture.orig.height;
|
||||
}
|
||||
|
||||
set height(height) {
|
||||
const s = Math.sign(this.scale.y) || 1;
|
||||
this.scale.y = s * height / this._texture.orig.height;
|
||||
this._height = height;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The texture that the sprite is using.
|
||||
* @type {PIXI.Texture}
|
||||
*/
|
||||
get texture() {
|
||||
return this._texture;
|
||||
}
|
||||
|
||||
set texture(texture) {
|
||||
texture = texture ?? null;
|
||||
if ( this._texture === texture ) return;
|
||||
if ( this._texture ) this._texture.off("update", this._onTextureUpdate, this);
|
||||
|
||||
this._texture = texture || PIXI.Texture.EMPTY;
|
||||
this._textureID = this._textureTrimmedID = -1;
|
||||
this._tintAlphaDirty = true;
|
||||
|
||||
if ( texture ) {
|
||||
if ( this._texture.baseTexture.valid ) this._onTextureUpdate();
|
||||
else this._texture.once("update", this._onTextureUpdate, this);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The anchor sets the origin point of the sprite. The default value is taken from the {@link PIXI.Texture|Texture}
|
||||
* and passed to the constructor.
|
||||
*
|
||||
* The default is `(0,0)`, this means the sprite's origin is the top left.
|
||||
*
|
||||
* Setting the anchor to `(0.5,0.5)` means the sprite's origin is centered.
|
||||
*
|
||||
* Setting the anchor to `(1,1)` would mean the sprite's origin point will be the bottom right corner.
|
||||
*
|
||||
* If you pass only single parameter, it will set both x and y to the same value as shown in the example below.
|
||||
* @type {PIXI.ObservablePoint}
|
||||
*/
|
||||
get anchor() {
|
||||
return this._anchor;
|
||||
}
|
||||
|
||||
set anchor(anchor) {
|
||||
this._anchor.copyFrom(anchor);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The tint applied to the sprite. This is a hex value.
|
||||
*
|
||||
* A value of 0xFFFFFF will remove any tint effect.
|
||||
* @type {number}
|
||||
* @defaultValue 0xFFFFFF
|
||||
*/
|
||||
get tint() {
|
||||
return this._tintColor.value;
|
||||
}
|
||||
|
||||
set tint(tint) {
|
||||
this._tintColor.setValue(tint);
|
||||
const tintRGB = this._tintColor.toLittleEndianNumber();
|
||||
if ( tintRGB === this._tintRGB ) return;
|
||||
this._tintRGB = tintRGB;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The HTML source element for this SpriteMesh texture.
|
||||
* @type {HTMLImageElement|HTMLVideoElement|null}
|
||||
*/
|
||||
get sourceElement() {
|
||||
if ( !this.texture.valid ) return null;
|
||||
return this.texture?.baseTexture.resource?.source || null;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this SpriteMesh rendering a video texture?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isVideo() {
|
||||
const source = this.sourceElement;
|
||||
return source?.tagName === "VIDEO";
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* When the texture is updated, this event will fire to update the scale and frame.
|
||||
* @protected
|
||||
*/
|
||||
_onTextureUpdate() {
|
||||
this._textureID = this._textureTrimmedID = this._transformID = this._transformTrimmedID = -1;
|
||||
if ( this._width ) this.scale.x = Math.sign(this.scale.x) * this._width / this._texture.orig.width;
|
||||
if ( this._height ) this.scale.y = Math.sign(this.scale.y) * this._height / this._texture.orig.height;
|
||||
// Alpha mode of the texture could have changed
|
||||
this._tintAlphaDirty = true;
|
||||
this.updateUvs();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Called when the anchor position updates.
|
||||
* @protected
|
||||
*/
|
||||
_onAnchorUpdate() {
|
||||
this._textureID = this._textureTrimmedID = this._transformID = this._transformTrimmedID = -1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update uvs and push vertices and uv buffers on GPU if necessary.
|
||||
*/
|
||||
updateUvs() {
|
||||
if ( this._textureID !== this._texture._updateID ) {
|
||||
let textureUvs;
|
||||
if ( (this._paddingX !== 0) || (this._paddingY !== 0) ) {
|
||||
const texture = this._texture;
|
||||
const frame = SpriteMesh.#TEMP_RECT.copyFrom(texture.frame).pad(this._paddingX, this._paddingY);
|
||||
textureUvs = this._textureUvs;
|
||||
textureUvs.set(frame, texture.baseTexture, texture.rotate);
|
||||
} else {
|
||||
textureUvs = this._texture._uvs;
|
||||
}
|
||||
this.uvs.set(textureUvs.uvsFloat32);
|
||||
this.#geometry.buffers[1].update();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize shader based on the shader class type.
|
||||
* @param {typeof BaseSamplerShader} shaderClass The shader class
|
||||
*/
|
||||
setShaderClass(shaderClass) {
|
||||
if ( !foundry.utils.isSubclass(shaderClass, BaseSamplerShader) ) {
|
||||
throw new Error("SpriteMesh shader class must inherit from BaseSamplerShader.");
|
||||
}
|
||||
if ( this._shader.constructor === shaderClass ) return;
|
||||
this._shader = shaderClass.create();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
updateTransform() {
|
||||
super.updateTransform();
|
||||
|
||||
// We set tintAlphaDirty to true if the worldAlpha has changed
|
||||
// It is needed to recompute the _cachedTint vec4 which is a combination of tint and alpha
|
||||
if ( this.#worldAlpha !== this.worldAlpha ) {
|
||||
this.#worldAlpha = this.worldAlpha;
|
||||
this._tintAlphaDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
#worldAlpha;
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Calculates worldTransform * vertices, store it in vertexData.
|
||||
*/
|
||||
calculateVertices() {
|
||||
if ( this._transformID === this.transform._worldID && this._textureID === this._texture._updateID ) return;
|
||||
|
||||
// Update uvs if necessary
|
||||
this.updateUvs();
|
||||
this._transformID = this.transform._worldID;
|
||||
this._textureID = this._texture._updateID;
|
||||
|
||||
// Set the vertex data
|
||||
const {a, b, c, d, tx, ty} = this.transform.worldTransform;
|
||||
const orig = this._texture.orig;
|
||||
const trim = this._texture.trim;
|
||||
const padX = this._paddingX;
|
||||
const padY = this._paddingY;
|
||||
|
||||
let w1; let w0; let h1; let h0;
|
||||
if ( trim ) {
|
||||
// If the sprite is trimmed and is not a tilingsprite then we need to add the extra
|
||||
// space before transforming the sprite coords
|
||||
w1 = trim.x - (this._anchor._x * orig.width) - padX;
|
||||
w0 = w1 + trim.width + (2 * padX);
|
||||
h1 = trim.y - (this._anchor._y * orig.height) - padY;
|
||||
h0 = h1 + trim.height + (2 * padY);
|
||||
}
|
||||
else {
|
||||
w1 = (-this._anchor._x * orig.width) - padX;
|
||||
w0 = w1 + orig.width + (2 * padX);
|
||||
h1 = (-this._anchor._y * orig.height) - padY;
|
||||
h0 = h1 + orig.height + (2 * padY);
|
||||
}
|
||||
|
||||
const vertexData = this.vertexData;
|
||||
vertexData[0] = (a * w1) + (c * h1) + tx;
|
||||
vertexData[1] = (d * h1) + (b * w1) + ty;
|
||||
vertexData[2] = (a * w0) + (c * h1) + tx;
|
||||
vertexData[3] = (d * h1) + (b * w0) + ty;
|
||||
vertexData[4] = (a * w0) + (c * h0) + tx;
|
||||
vertexData[5] = (d * h0) + (b * w0) + ty;
|
||||
vertexData[6] = (a * w1) + (c * h0) + tx;
|
||||
vertexData[7] = (d * h0) + (b * w1) + ty;
|
||||
|
||||
if ( this.roundPixels ) {
|
||||
const r = PIXI.settings.RESOLUTION;
|
||||
for ( let i = 0; i < vertexData.length; ++i ) vertexData[i] = Math.round(vertexData[i] * r) / r;
|
||||
}
|
||||
this.#geometry.buffers[0].update();
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Calculates worldTransform * vertices for a non texture with a trim. store it in vertexTrimmedData.
|
||||
*
|
||||
* This is used to ensure that the true width and height of a trimmed texture is respected.
|
||||
*/
|
||||
calculateTrimmedVertices() {
|
||||
if ( !this.vertexTrimmedData ) this.vertexTrimmedData = new Float32Array(8);
|
||||
else if ( (this._transformTrimmedID === this.transform._worldID)
|
||||
&& (this._textureTrimmedID === this._texture._updateID) ) return;
|
||||
|
||||
this._transformTrimmedID = this.transform._worldID;
|
||||
this._textureTrimmedID = this._texture._updateID;
|
||||
|
||||
const texture = this._texture;
|
||||
const vertexData = this.vertexTrimmedData;
|
||||
const orig = texture.orig;
|
||||
const anchor = this._anchor;
|
||||
const padX = this._paddingX;
|
||||
const padY = this._paddingY;
|
||||
|
||||
// Compute the new untrimmed bounds
|
||||
const wt = this.transform.worldTransform;
|
||||
const a = wt.a;
|
||||
const b = wt.b;
|
||||
const c = wt.c;
|
||||
const d = wt.d;
|
||||
const tx = wt.tx;
|
||||
const ty = wt.ty;
|
||||
|
||||
const w1 = (-anchor._x * orig.width) - padX;
|
||||
const w0 = w1 + orig.width + (2 * padX);
|
||||
const h1 = (-anchor._y * orig.height) - padY;
|
||||
const h0 = h1 + orig.height + (2 * padY);
|
||||
|
||||
vertexData[0] = (a * w1) + (c * h1) + tx;
|
||||
vertexData[1] = (d * h1) + (b * w1) + ty;
|
||||
vertexData[2] = (a * w0) + (c * h1) + tx;
|
||||
vertexData[3] = (d * h1) + (b * w0) + ty;
|
||||
vertexData[4] = (a * w0) + (c * h0) + tx;
|
||||
vertexData[5] = (d * h0) + (b * w0) + ty;
|
||||
vertexData[6] = (a * w1) + (c * h0) + tx;
|
||||
vertexData[7] = (d * h0) + (b * w1) + ty;
|
||||
|
||||
if ( this.roundPixels ) {
|
||||
const r = PIXI.settings.RESOLUTION;
|
||||
for ( let i = 0; i < vertexData.length; ++i ) vertexData[i] = Math.round(vertexData[i] * r) / r;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_render(renderer) {
|
||||
const pluginName = this.pluginName;
|
||||
if ( pluginName ) this.#renderBatched(renderer, pluginName);
|
||||
else this.#renderDirect(renderer, this._shader);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render with batching.
|
||||
* @param {PIXI.Renderer} renderer The renderer
|
||||
* @param {string} pluginName The batch renderer
|
||||
*/
|
||||
#renderBatched(renderer, pluginName) {
|
||||
this.calculateVertices();
|
||||
this._updateBatchData();
|
||||
const batchRenderer = renderer.plugins[pluginName];
|
||||
renderer.batch.setObjectRenderer(batchRenderer);
|
||||
batchRenderer.render(this._batchData);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render without batching.
|
||||
* @param {PIXI.Renderer} renderer The renderer
|
||||
* @param {BaseSamplerShader} shader The shader
|
||||
*/
|
||||
#renderDirect(renderer, shader) {
|
||||
this.calculateVertices();
|
||||
if ( this._tintAlphaDirty ) {
|
||||
PIXI.Color.shared.setValue(this._tintColor)
|
||||
.premultiply(this.worldAlpha, this.alphaMode > 0)
|
||||
.toArray(this._cachedTint);
|
||||
this._tintAlphaDirty = false;
|
||||
}
|
||||
shader._preRender(this, renderer);
|
||||
renderer.batch.flush();
|
||||
renderer.shader.bind(shader);
|
||||
renderer.state.set(this.#state);
|
||||
renderer.geometry.bind(this.#geometry, shader);
|
||||
renderer.geometry.draw(PIXI.DRAW_MODES.TRIANGLES, 6, 0);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the batch data object.
|
||||
* @protected
|
||||
*/
|
||||
_updateBatchData() {
|
||||
this._batchData._texture = this._texture;
|
||||
this._batchData.worldAlpha = this.worldAlpha;
|
||||
this._batchData._tintRGB = this._tintRGB;
|
||||
this._batchData.blendMode = this.#state.blendMode;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_calculateBounds() {
|
||||
const trim = this._texture.trim;
|
||||
const orig = this._texture.orig;
|
||||
|
||||
// First lets check to see if the current texture has a trim.
|
||||
if ( !trim || ((trim.width === orig.width) && (trim.height === orig.height)) ) {
|
||||
this.calculateVertices();
|
||||
this._bounds.addQuad(this.vertexData);
|
||||
}
|
||||
else {
|
||||
this.calculateTrimmedVertices();
|
||||
this._bounds.addQuad(this.vertexTrimmedData);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getLocalBounds(rect) {
|
||||
// Fast local bounds computation if the sprite has no children!
|
||||
if ( this.children.length === 0 ) {
|
||||
if ( !this._localBounds ) this._localBounds = new PIXI.Bounds();
|
||||
|
||||
const padX = this._paddingX;
|
||||
const padY = this._paddingY;
|
||||
const orig = this._texture.orig;
|
||||
this._localBounds.minX = (orig.width * -this._anchor._x) - padX;
|
||||
this._localBounds.minY = (orig.height * -this._anchor._y) - padY;
|
||||
this._localBounds.maxX = (orig.width * (1 - this._anchor._x)) + padX;
|
||||
this._localBounds.maxY = (orig.height * (1 - this._anchor._y)) + padY;
|
||||
|
||||
if ( !rect ) {
|
||||
if ( !this._localBoundsRect ) this._localBoundsRect = new PIXI.Rectangle();
|
||||
rect = this._localBoundsRect;
|
||||
}
|
||||
|
||||
return this._localBounds.getRectangle(rect);
|
||||
}
|
||||
|
||||
return super.getLocalBounds(rect);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
containsPoint(point) {
|
||||
const tempPoint = SpriteMesh.#TEMP_POINT;
|
||||
this.worldTransform.applyInverse(point, tempPoint);
|
||||
|
||||
const width = this._texture.orig.width;
|
||||
const height = this._texture.orig.height;
|
||||
const x1 = -width * this.anchor.x;
|
||||
let y1 = 0;
|
||||
|
||||
if ( (tempPoint.x >= x1) && (tempPoint.x < (x1 + width)) ) {
|
||||
y1 = -height * this.anchor.y;
|
||||
if ( (tempPoint.y >= y1) && (tempPoint.y < (y1 + height)) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
destroy(options) {
|
||||
super.destroy(options);
|
||||
this.#geometry.dispose();
|
||||
this.#geometry = null;
|
||||
this._shader = null;
|
||||
this.#state = null;
|
||||
this.uvs = null;
|
||||
this.indices = null;
|
||||
this.vertexData = null;
|
||||
this._texture.off("update", this._onTextureUpdate, this);
|
||||
this._anchor = null;
|
||||
const destroyTexture = (typeof options === "boolean" ? options : options?.texture);
|
||||
if ( destroyTexture ) {
|
||||
const destroyBaseTexture = (typeof options === "boolean" ? options : options?.baseTexture);
|
||||
this._texture.destroy(!!destroyBaseTexture);
|
||||
}
|
||||
this._texture = null;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a SpriteMesh from another source.
|
||||
* You can specify texture options and a specific shader class derived from BaseSamplerShader.
|
||||
* @param {string|PIXI.Texture|HTMLCanvasElement|HTMLVideoElement} source Source to create texture from.
|
||||
* @param {object} [textureOptions] See {@link PIXI.BaseTexture}'s constructor for options.
|
||||
* @param {BaseSamplerShader} [shaderClass] The shader class to use. BaseSamplerShader by default.
|
||||
* @returns {SpriteMesh}
|
||||
*/
|
||||
static from(source, textureOptions, shaderClass) {
|
||||
const texture = source instanceof PIXI.Texture ? source : PIXI.Texture.from(source, textureOptions);
|
||||
return new SpriteMesh(texture, shaderClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* UnboundContainers behave like PIXI.Containers except that they are not bound to their parent's transforms.
|
||||
* However, they normally propagate their own transformations to their children.
|
||||
*/
|
||||
class UnboundContainer extends PIXI.Container {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
// Replacing PIXI.Transform with an UnboundTransform
|
||||
this.transform = new UnboundTransform();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A custom Transform class which is not bound to the parent worldTransform.
|
||||
* localTransform are working as usual.
|
||||
*/
|
||||
class UnboundTransform extends PIXI.Transform {
|
||||
/** @override */
|
||||
static IDENTITY = new UnboundTransform();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
updateTransform(parentTransform) {
|
||||
const lt = this.localTransform;
|
||||
|
||||
if ( this._localID !== this._currentLocalID ) {
|
||||
// Get the matrix values of the displayobject based on its transform properties..
|
||||
lt.a = this._cx * this.scale.x;
|
||||
lt.b = this._sx * this.scale.x;
|
||||
lt.c = this._cy * this.scale.y;
|
||||
lt.d = this._sy * this.scale.y;
|
||||
|
||||
lt.tx = this.position.x - ((this.pivot.x * lt.a) + (this.pivot.y * lt.c));
|
||||
lt.ty = this.position.y - ((this.pivot.x * lt.b) + (this.pivot.y * lt.d));
|
||||
this._currentLocalID = this._localID;
|
||||
|
||||
// Force an update
|
||||
this._parentID = -1;
|
||||
}
|
||||
|
||||
if ( this._parentID !== parentTransform._worldID ) {
|
||||
// We don't use the values from the parent transform. We're just updating IDs.
|
||||
this._parentID = parentTransform._worldID;
|
||||
this._worldID++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user