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

View File

@@ -0,0 +1,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;
}
});

View 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);
}
}

View File

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

View 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);
}
}

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

View 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});
}
}

View 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);
}
}

View File

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