Files
Foundry-VTT-Docker/resources/app/client/pixi/groups/interface.js
2025-01-04 00:34:03 +01:00

235 lines
8.3 KiB
JavaScript

/**
* A container group which displays interface elements rendered above other canvas groups.
* @extends {CanvasGroupMixin(PIXI.Container)}
*/
class InterfaceCanvasGroup extends CanvasGroupMixin(PIXI.Container) {
/** @override */
static groupName = "interface";
/**
* A container dedicated to the display of scrolling text.
* @type {PIXI.Container}
*/
#scrollingText;
/**
* A graphics which represent the scene outline.
* @type {PIXI.Graphics}
*/
#outline;
/**
* The interface drawings container.
* @type {PIXI.Container}
*/
#drawings;
/* -------------------------------------------- */
/* Drawing Management */
/* -------------------------------------------- */
/**
* Add a PrimaryGraphics to the group.
* @param {Drawing} drawing The Drawing being added
* @returns {PIXI.Graphics} The created Graphics instance
*/
addDrawing(drawing) {
const name = drawing.objectId;
const shape = this.drawings.graphics.get(name) ?? this.#drawings.addChild(new PIXI.Graphics());
shape.name = name;
this.drawings.graphics.set(name, shape);
return shape;
}
/* -------------------------------------------- */
/**
* Remove a PrimaryGraphics from the group.
* @param {Drawing} drawing The Drawing being removed
*/
removeDrawing(drawing) {
const name = drawing.objectId;
if ( !this.drawings.graphics.has(name) ) return;
const shape = this.drawings.graphics.get(name);
if ( shape?.destroyed === false ) shape.destroy({children: true});
this.drawings.graphics.delete(name);
}
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/** @inheritDoc */
async _draw(options) {
this.#drawOutline();
this.#createInterfaceDrawingsContainer();
this.#drawScrollingText();
await super._draw(options);
// Necessary so that Token#voidMesh don't earse non-interface elements
this.filters = [new VoidFilter()];
this.filterArea = canvas.app.screen;
}
/* -------------------------------------------- */
/**
* Draw a background outline which emphasizes what portion of the canvas is playable space and what is buffer.
*/
#drawOutline() {
// Create Canvas outline
const outline = this.#outline = this.addChild(new PIXI.Graphics());
const {scene, dimensions} = canvas;
const displayCanvasBorder = scene.padding !== 0;
const displaySceneOutline = !scene.background.src;
if ( !(displayCanvasBorder || displaySceneOutline) ) return;
if ( displayCanvasBorder ) outline.lineStyle({
alignment: 1,
alpha: 0.75,
color: 0x000000,
join: PIXI.LINE_JOIN.BEVEL,
width: 4
}).drawShape(dimensions.rect);
if ( displaySceneOutline ) outline.lineStyle({
alignment: 1,
alpha: 0.25,
color: 0x000000,
join: PIXI.LINE_JOIN.BEVEL,
width: 4
}).drawShape(dimensions.sceneRect).endFill();
}
/* -------------------------------------------- */
/* Scrolling Text */
/* -------------------------------------------- */
/**
* Draw the scrolling text.
*/
#drawScrollingText() {
this.#scrollingText = this.addChild(new PIXI.Container());
const {width, height} = canvas.dimensions;
this.#scrollingText.width = width;
this.#scrollingText.height = height;
this.#scrollingText.eventMode = "none";
this.#scrollingText.interactiveChildren = false;
this.#scrollingText.zIndex = CONFIG.Canvas.groups.interface.zIndexScrollingText;
}
/* -------------------------------------------- */
/**
* Create the interface drawings container.
*/
#createInterfaceDrawingsContainer() {
this.#drawings = this.addChild(new PIXI.Container());
this.#drawings.sortChildren = function() {
const children = this.children;
for ( let i = 0, n = children.length; i < n; i++ ) children[i]._lastSortedIndex = i;
children.sort(InterfaceCanvasGroup.#compareObjects);
this.sortDirty = false;
};
this.#drawings.sortableChildren = true;
this.#drawings.eventMode = "none";
this.#drawings.interactiveChildren = false;
this.#drawings.zIndex = CONFIG.Canvas.groups.interface.zIndexDrawings;
}
/* -------------------------------------------- */
/**
* The sorting function used to order objects inside the Interface Drawings Container
* Overrides the default sorting function defined for the PIXI.Container.
* @param {PrimaryCanvasObject|PIXI.DisplayObject} a An object to display
* @param {PrimaryCanvasObject|PIXI.DisplayObject} b Some other object to display
* @returns {number}
*/
static #compareObjects(a, b) {
return ((a.elevation || 0) - (b.elevation || 0))
|| ((a.sort || 0) - (b.sort || 0))
|| (a.zIndex - b.zIndex)
|| (a._lastSortedIndex - b._lastSortedIndex);
}
/* -------------------------------------------- */
/**
* Display scrolling status text originating from an origin point on the Canvas.
* @param {Point} origin An origin point where the text should first emerge
* @param {string} content The text content to display
* @param {object} [options] Options which customize the text animation
* @param {number} [options.duration=2000] The duration of the scrolling effect in milliseconds
* @param {number} [options.distance] The distance in pixels that the scrolling text should travel
* @param {TEXT_ANCHOR_POINTS} [options.anchor] The original anchor point where the text appears
* @param {TEXT_ANCHOR_POINTS} [options.direction] The direction in which the text scrolls
* @param {number} [options.jitter=0] An amount of randomization between [0, 1] applied to the initial position
* @param {object} [options.textStyle={}] Additional parameters of PIXI.TextStyle which are applied to the text
* @returns {Promise<PreciseText|null>} The created PreciseText object which is scrolling
*/
async createScrollingText(origin, content, {duration=2000, distance, jitter=0, anchor, direction, ...textStyle}={}) {
if ( !game.settings.get("core", "scrollingStatusText") ) return null;
// Create text object
const style = PreciseText.getTextStyle({anchor, ...textStyle});
const text = this.#scrollingText.addChild(new PreciseText(content, style));
text.visible = false;
// Set initial coordinates
const jx = (jitter ? (Math.random()-0.5) * jitter : 0) * text.width;
const jy = (jitter ? (Math.random()-0.5) * jitter : 0) * text.height;
text.position.set(origin.x + jx, origin.y + jy);
// Configure anchor point
text.anchor.set(...{
[CONST.TEXT_ANCHOR_POINTS.CENTER]: [0.5, 0.5],
[CONST.TEXT_ANCHOR_POINTS.BOTTOM]: [0.5, 0],
[CONST.TEXT_ANCHOR_POINTS.TOP]: [0.5, 1],
[CONST.TEXT_ANCHOR_POINTS.LEFT]: [1, 0.5],
[CONST.TEXT_ANCHOR_POINTS.RIGHT]: [0, 0.5]
}[anchor ?? CONST.TEXT_ANCHOR_POINTS.CENTER]);
// Configure animation distance
let dx = 0;
let dy = 0;
switch ( direction ?? CONST.TEXT_ANCHOR_POINTS.TOP ) {
case CONST.TEXT_ANCHOR_POINTS.BOTTOM:
dy = distance ?? (2 * text.height); break;
case CONST.TEXT_ANCHOR_POINTS.TOP:
dy = -1 * (distance ?? (2 * text.height)); break;
case CONST.TEXT_ANCHOR_POINTS.LEFT:
dx = -1 * (distance ?? (2 * text.width)); break;
case CONST.TEXT_ANCHOR_POINTS.RIGHT:
dx = distance ?? (2 * text.width); break;
}
// Fade In
await CanvasAnimation.animate([
{parent: text, attribute: "alpha", from: 0, to: 1.0},
{parent: text.scale, attribute: "x", from: 0.6, to: 1.0},
{parent: text.scale, attribute: "y", from: 0.6, to: 1.0}
], {
context: this,
duration: duration * 0.25,
easing: CanvasAnimation.easeInOutCosine,
ontick: () => text.visible = true
});
// Scroll
const scroll = [{parent: text, attribute: "alpha", to: 0.0}];
if ( dx !== 0 ) scroll.push({parent: text, attribute: "x", to: text.position.x + dx});
if ( dy !== 0 ) scroll.push({parent: text, attribute: "y", to: text.position.y + dy});
await CanvasAnimation.animate(scroll, {
context: this,
duration: duration * 0.75,
easing: CanvasAnimation.easeInOutCosine
});
// Clean-up
this.#scrollingText.removeChild(text);
text.destroy();
}
}