/** * 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} 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(); } }