Initial
This commit is contained in:
498
resources/app/client/pixi/groups/effects.js
vendored
Normal file
498
resources/app/client/pixi/groups/effects.js
vendored
Normal file
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* @typedef {foundry.utils.Collection} EffectsCollection
|
||||
*/
|
||||
|
||||
/**
|
||||
* A container group which contains visual effects rendered above the primary group.
|
||||
*
|
||||
* TODO:
|
||||
* The effects canvas group is now only performing shape initialization, logic that needs to happen at
|
||||
* the placeable or object level is now their burden.
|
||||
* - [DONE] Adding or removing a source from the EffectsCanvasGroup collection.
|
||||
* - [TODO] A change in a darkness source should re-initialize all overlaping light and vision source.
|
||||
*
|
||||
* ### Hook Events
|
||||
* - {@link hookEvents.lightingRefresh}
|
||||
*
|
||||
* @category - Canvas
|
||||
*/
|
||||
class EffectsCanvasGroup extends CanvasGroupMixin(PIXI.Container) {
|
||||
|
||||
/**
|
||||
* The name of the darkness level animation.
|
||||
* @type {string}
|
||||
*/
|
||||
static #DARKNESS_ANIMATION_NAME = "lighting.animateDarkness";
|
||||
|
||||
/**
|
||||
* Whether to currently animate light sources.
|
||||
* @type {boolean}
|
||||
*/
|
||||
animateLightSources = true;
|
||||
|
||||
/**
|
||||
* Whether to currently animate vision sources.
|
||||
* @type {boolean}
|
||||
*/
|
||||
animateVisionSources = true;
|
||||
|
||||
/**
|
||||
* A mapping of light sources which are active within the rendered Scene.
|
||||
* @type {EffectsCollection<string, PointLightSource>}
|
||||
*/
|
||||
lightSources = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* A mapping of darkness sources which are active within the rendered Scene.
|
||||
* @type {EffectsCollection<string, PointDarknessSource>}
|
||||
*/
|
||||
darknessSources = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* A Collection of vision sources which are currently active within the rendered Scene.
|
||||
* @type {EffectsCollection<string, PointVisionSource>}
|
||||
*/
|
||||
visionSources = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* A set of vision mask filters used in visual effects group
|
||||
* @type {Set<VisualEffectsMaskingFilter>}
|
||||
*/
|
||||
visualEffectsMaskingFilters = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Iterator for all light and darkness sources.
|
||||
* @returns {Generator<PointDarknessSource|PointLightSource, void, void>}
|
||||
* @yields foundry.canvas.sources.PointDarknessSource|foundry.canvas.sources.PointLightSource
|
||||
*/
|
||||
* allSources() {
|
||||
for ( const darknessSource of this.darknessSources ) yield darknessSource;
|
||||
for ( const lightSource of this.lightSources ) yield lightSource;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_createLayers() {
|
||||
/**
|
||||
* A layer of background alteration effects which change the appearance of the primary group render texture.
|
||||
* @type {CanvasBackgroundAlterationEffects}
|
||||
*/
|
||||
this.background = this.addChild(new CanvasBackgroundAlterationEffects());
|
||||
|
||||
/**
|
||||
* A layer which adds illumination-based effects to the scene.
|
||||
* @type {CanvasIlluminationEffects}
|
||||
*/
|
||||
this.illumination = this.addChild(new CanvasIlluminationEffects());
|
||||
|
||||
/**
|
||||
* A layer which adds color-based effects to the scene.
|
||||
* @type {CanvasColorationEffects}
|
||||
*/
|
||||
this.coloration = this.addChild(new CanvasColorationEffects());
|
||||
|
||||
/**
|
||||
* A layer which adds darkness effects to the scene.
|
||||
* @type {CanvasDarknessEffects}
|
||||
*/
|
||||
this.darkness = this.addChild(new CanvasDarknessEffects());
|
||||
|
||||
return {
|
||||
background: this.background,
|
||||
illumination: this.illumination,
|
||||
coloration: this.coloration,
|
||||
darkness: this.darkness
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Clear all effects containers and animated sources.
|
||||
*/
|
||||
clearEffects() {
|
||||
this.background.clear();
|
||||
this.illumination.clear();
|
||||
this.coloration.clear();
|
||||
this.darkness.clear();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _draw(options) {
|
||||
// Draw each component layer
|
||||
await this.background.draw();
|
||||
await this.illumination.draw();
|
||||
await this.coloration.draw();
|
||||
await this.darkness.draw();
|
||||
|
||||
// Call hooks
|
||||
Hooks.callAll("drawEffectsCanvasGroup", this);
|
||||
|
||||
// Activate animation of drawn objects
|
||||
this.activateAnimation();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Perception Management Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize positive light sources which exist within the active Scene.
|
||||
* Packages can use the "initializeLightSources" hook to programmatically add light sources.
|
||||
*/
|
||||
initializeLightSources() {
|
||||
for ( let source of this.lightSources ) source.initialize();
|
||||
Hooks.callAll("initializeLightSources", this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Re-initialize the shapes of all darkness sources in the Scene.
|
||||
* This happens before initialization of light sources because darkness sources contribute additional edges which
|
||||
* limit perception.
|
||||
* Packages can use the "initializeDarknessSources" hook to programmatically add darkness sources.
|
||||
*/
|
||||
initializeDarknessSources() {
|
||||
for ( let source of this.darknessSources ) source.initialize();
|
||||
Hooks.callAll("initializeDarknessSources", this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the state and uniforms of all light sources and darkness sources objects.
|
||||
*/
|
||||
refreshLightSources() {
|
||||
for ( const source of this.allSources() ) source.refresh();
|
||||
// FIXME: We need to refresh the field of an AmbientLight only after the initialization of the light source when
|
||||
// the shape of the source could have changed. We don't need to refresh all fields whenever lighting is refreshed.
|
||||
canvas.lighting.refreshFields();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the state and uniforms of all VisionSource objects.
|
||||
*/
|
||||
refreshVisionSources() {
|
||||
for ( const visionSource of this.visionSources ) visionSource.refresh();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the active display of lighting.
|
||||
*/
|
||||
refreshLighting() {
|
||||
|
||||
// Apply illumination and visibility background color change
|
||||
this.illumination.backgroundColor = canvas.colors.background;
|
||||
if ( this.illumination.darknessLevelMeshes.clearColor[0] !== canvas.environment.darknessLevel ) {
|
||||
this.illumination.darknessLevelMeshes.clearColor[0] = canvas.environment.darknessLevel;
|
||||
this.illumination.invalidateDarknessLevelContainer(true);
|
||||
}
|
||||
const v = canvas.visibility.filter;
|
||||
if ( v ) {
|
||||
v.uniforms.visionTexture = canvas.masks.vision.renderTexture;
|
||||
v.uniforms.primaryTexture = canvas.primary.renderTexture;
|
||||
canvas.colors.fogExplored.applyRGB(v.uniforms.exploredColor);
|
||||
canvas.colors.fogUnexplored.applyRGB(v.uniforms.unexploredColor);
|
||||
canvas.colors.background.applyRGB(v.uniforms.backgroundColor);
|
||||
}
|
||||
|
||||
// Clear effects
|
||||
canvas.effects.clearEffects();
|
||||
|
||||
// Add effect meshes for active light and darkness sources
|
||||
for ( const source of this.allSources() ) this.#addLightEffect(source);
|
||||
|
||||
// Add effect meshes for active vision sources
|
||||
for ( const visionSource of this.visionSources ) this.#addVisionEffect(visionSource);
|
||||
|
||||
// Update vision filters state
|
||||
this.background.vision.filter.enabled = !!this.background.vision.children.length;
|
||||
this.background.visionPreferred.filter.enabled = !!this.background.visionPreferred.children.length;
|
||||
|
||||
// Hide the background and/or coloration layers if possible
|
||||
const lightingOptions = canvas.visibility.visionModeData.activeLightingOptions;
|
||||
this.background.vision.visible = (this.background.vision.children.length > 0);
|
||||
this.background.visionPreferred.visible = (this.background.visionPreferred.children.length > 0);
|
||||
this.background.lighting.visible = (this.background.lighting.children.length > 0)
|
||||
|| (lightingOptions.background?.postProcessingModes?.length > 0);
|
||||
this.coloration.visible = (this.coloration.children.length > 1)
|
||||
|| (lightingOptions.coloration?.postProcessingModes?.length > 0);
|
||||
|
||||
// Call hooks
|
||||
Hooks.callAll("lightingRefresh", this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a vision source to the effect layers.
|
||||
* @param {RenderedEffectSource & PointVisionSource} source The vision source to add mesh layers
|
||||
*/
|
||||
#addVisionEffect(source) {
|
||||
if ( !source.active || (source.radius <= 0) ) return;
|
||||
const meshes = source.drawMeshes();
|
||||
if ( meshes.background ) {
|
||||
// Is this vision source background need to be rendered into the preferred vision container, over other VS?
|
||||
const parent = source.preferred ? this.background.visionPreferred : this.background.vision;
|
||||
parent.addChild(meshes.background);
|
||||
}
|
||||
if ( meshes.illumination ) this.illumination.lights.addChild(meshes.illumination);
|
||||
if ( meshes.coloration ) this.coloration.addChild(meshes.coloration);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a light source or a darkness source to the effect layers
|
||||
* @param {RenderedEffectSource & BaseLightSource} source The light or darkness source to add to the effect layers.
|
||||
*/
|
||||
#addLightEffect(source) {
|
||||
if ( !source.active ) return;
|
||||
const meshes = source.drawMeshes();
|
||||
if ( meshes.background ) this.background.lighting.addChild(meshes.background);
|
||||
if ( meshes.illumination ) this.illumination.lights.addChild(meshes.illumination);
|
||||
if ( meshes.coloration ) this.coloration.addChild(meshes.coloration);
|
||||
if ( meshes.darkness ) this.darkness.addChild(meshes.darkness);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the point is inside light.
|
||||
* @param {Point} point The point.
|
||||
* @param {number} elevation The elevation of the point.
|
||||
* @returns {boolean} Is inside light?
|
||||
*/
|
||||
testInsideLight(point, elevation) {
|
||||
|
||||
// First test light source excluding the global light source
|
||||
for ( const lightSource of this.lightSources ) {
|
||||
if ( !lightSource.active || (lightSource instanceof foundry.canvas.sources.GlobalLightSource) ) continue;
|
||||
if ( lightSource.shape.contains(point.x, point.y) ) return true;
|
||||
}
|
||||
|
||||
// Second test Global Illumination and Darkness Level meshes
|
||||
const globalLightSource = canvas.environment.globalLightSource;
|
||||
if ( !globalLightSource.active ) return false;
|
||||
const {min, max} = globalLightSource.data.darkness;
|
||||
const darknessLevel = this.getDarknessLevel(point, elevation);
|
||||
return (darknessLevel >= min) && (darknessLevel <= max);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the point is inside darkness.
|
||||
* @param {Point} point The point.
|
||||
* @param {number} elevation The elevation of the point.
|
||||
* @returns {boolean} Is inside a darkness?
|
||||
*/
|
||||
testInsideDarkness({x, y}, elevation) {
|
||||
for ( const source of this.darknessSources ) {
|
||||
if ( !source.active || source.isPreview ) continue;
|
||||
for ( let dx = -1; dx <= 1; dx += 1 ) {
|
||||
for ( let dy = -1; dy <= 1; dy += 1 ) {
|
||||
if ( source.shape.contains(x + dx, y + dy) ) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the darkness level at the given point.
|
||||
* @param {Point} point The point.
|
||||
* @param {number} elevation The elevation of the point.
|
||||
* @returns {number} The darkness level.
|
||||
*/
|
||||
getDarknessLevel(point, elevation) {
|
||||
const darknessLevelMeshes = canvas.effects.illumination.darknessLevelMeshes.children;
|
||||
for ( let i = darknessLevelMeshes.length - 1; i >= 0; i-- ) {
|
||||
const darknessLevelMesh = darknessLevelMeshes[i];
|
||||
if ( darknessLevelMesh.region.testPoint(point, elevation) ) {
|
||||
return darknessLevelMesh.shader.uniforms.darknessLevel;
|
||||
}
|
||||
}
|
||||
return canvas.environment.darknessLevel;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _tearDown(options) {
|
||||
CanvasAnimation.terminateAnimation(EffectsCanvasGroup.#DARKNESS_ANIMATION_NAME);
|
||||
this.deactivateAnimation();
|
||||
this.darknessSources.clear();
|
||||
this.lightSources.clear();
|
||||
for ( const c of this.children ) {
|
||||
if ( c.clear ) c.clear();
|
||||
else if ( c.tearDown ) await c.tearDown();
|
||||
else c.destroy();
|
||||
}
|
||||
this.visualEffectsMaskingFilters.clear();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate vision masking for visual effects
|
||||
* @param {boolean} [enabled=true] Whether to enable or disable vision masking
|
||||
*/
|
||||
toggleMaskingFilters(enabled=true) {
|
||||
for ( const f of this.visualEffectsMaskingFilters ) {
|
||||
f.uniforms.enableVisionMasking = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate post-processing effects for a certain effects channel.
|
||||
* @param {string} filterMode The filter mode to target.
|
||||
* @param {string[]} [postProcessingModes=[]] The post-processing modes to apply to this filter.
|
||||
* @param {Object} [uniforms={}] The uniforms to update.
|
||||
*/
|
||||
activatePostProcessingFilters(filterMode, postProcessingModes=[], uniforms={}) {
|
||||
for ( const f of this.visualEffectsMaskingFilters ) {
|
||||
if ( f.uniforms.mode === filterMode ) {
|
||||
f.updatePostprocessModes(postProcessingModes, uniforms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Reset post-processing modes on all Visual Effects masking filters.
|
||||
*/
|
||||
resetPostProcessingFilters() {
|
||||
for ( const f of this.visualEffectsMaskingFilters ) {
|
||||
f.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Animation Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate light source animation for AmbientLight objects within this layer
|
||||
*/
|
||||
activateAnimation() {
|
||||
this.deactivateAnimation();
|
||||
if ( game.settings.get("core", "lightAnimation") === false ) return;
|
||||
canvas.app.ticker.add(this.#animateSources, this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Deactivate light source animation for AmbientLight objects within this layer
|
||||
*/
|
||||
deactivateAnimation() {
|
||||
canvas.app.ticker.remove(this.#animateSources, this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The ticker handler which manages animation delegation
|
||||
* @param {number} dt Delta time
|
||||
* @private
|
||||
*/
|
||||
#animateSources(dt) {
|
||||
|
||||
// Animate light and darkness sources
|
||||
if ( this.animateLightSources ) {
|
||||
for ( const source of this.allSources() ) {
|
||||
source.animate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Animate vision sources
|
||||
if ( this.animateVisionSources ) {
|
||||
for ( const source of this.visionSources.values() ) {
|
||||
source.animate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Animate a smooth transition of the darkness overlay to a target value.
|
||||
* Only begin animating if another animation is not already in progress.
|
||||
* @param {number} target The target darkness level between 0 and 1
|
||||
* @param {number} duration The desired animation time in milliseconds. Default is 10 seconds
|
||||
* @returns {Promise} A Promise which resolves once the animation is complete
|
||||
*/
|
||||
async animateDarkness(target=1.0, {duration=10000}={}) {
|
||||
CanvasAnimation.terminateAnimation(EffectsCanvasGroup.#DARKNESS_ANIMATION_NAME);
|
||||
if ( target === canvas.environment.darknessLevel ) return false;
|
||||
if ( duration <= 0 ) return canvas.environment.initialize({environment: {darknessLevel: target}});
|
||||
|
||||
// Update with an animation
|
||||
const animationData = [{
|
||||
parent: {darkness: canvas.environment.darknessLevel},
|
||||
attribute: "darkness",
|
||||
to: Math.clamp(target, 0, 1)
|
||||
}];
|
||||
return CanvasAnimation.animate(animationData, {
|
||||
name: EffectsCanvasGroup.#DARKNESS_ANIMATION_NAME,
|
||||
duration: duration,
|
||||
ontick: (dt, animation) =>
|
||||
canvas.environment.initialize({environment: {darknessLevel: animation.attributes[0].parent.darkness}})
|
||||
}).then(completed => {
|
||||
if ( !completed ) canvas.environment.initialize({environment: {darknessLevel: target}});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get visibility() {
|
||||
const msg = "EffectsCanvasGroup#visibility has been deprecated and moved to " +
|
||||
"Canvas#visibility.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return canvas.visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get globalLightSource() {
|
||||
const msg = "EffectsCanvasGroup#globalLightSource has been deprecated and moved to " +
|
||||
"EnvironmentCanvasGroup#globalLightSource.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return canvas.environment.globalLightSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
updateGlobalLightSource() {
|
||||
const msg = "EffectsCanvasGroup#updateGlobalLightSource has been deprecated and is part of " +
|
||||
"EnvironmentCanvasGroup#initialize workflow.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
canvas.environment.initialize();
|
||||
}
|
||||
}
|
||||
328
resources/app/client/pixi/groups/environment.js
Normal file
328
resources/app/client/pixi/groups/environment.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* A container group which contains the primary canvas group and the effects canvas group.
|
||||
*
|
||||
* @category - Canvas
|
||||
*/
|
||||
class EnvironmentCanvasGroup extends CanvasGroupMixin(PIXI.Container) {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.eventMode = "static";
|
||||
|
||||
/**
|
||||
* The global light source attached to the environment
|
||||
* @type {GlobalLightSource}
|
||||
*/
|
||||
Object.defineProperty(this, "globalLightSource", {
|
||||
value: new CONFIG.Canvas.globalLightSourceClass({object: this, sourceId: "globalLight"}),
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static groupName = "environment";
|
||||
|
||||
/** @override */
|
||||
static tearDownChildren = false;
|
||||
|
||||
/**
|
||||
* The scene darkness level.
|
||||
* @type {number}
|
||||
*/
|
||||
#darknessLevel;
|
||||
|
||||
/**
|
||||
* Colors exposed by the manager.
|
||||
* @enum {Color}
|
||||
*/
|
||||
colors = {
|
||||
darkness: undefined,
|
||||
halfdark: undefined,
|
||||
background: undefined,
|
||||
dim: undefined,
|
||||
bright: undefined,
|
||||
ambientBrightest: undefined,
|
||||
ambientDaylight: undefined,
|
||||
ambientDarkness: undefined,
|
||||
sceneBackground: undefined,
|
||||
fogExplored: undefined,
|
||||
fogUnexplored: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* Weights used by the manager to compute colors.
|
||||
* @enum {number}
|
||||
*/
|
||||
weights = {
|
||||
dark: undefined,
|
||||
halfdark: undefined,
|
||||
dim: undefined,
|
||||
bright: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback colors.
|
||||
* @enum {Color}
|
||||
*/
|
||||
static #fallbackColors = {
|
||||
darknessColor: 0x242448,
|
||||
daylightColor: 0xEEEEEE,
|
||||
brightestColor: 0xFFFFFF,
|
||||
backgroundColor: 0x999999,
|
||||
fogUnexplored: 0x000000,
|
||||
fogExplored: 0x000000
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains a list of subscribed function for darkness handler.
|
||||
* @type {PIXI.EventBoundary}
|
||||
*/
|
||||
#eventBoundary;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the darkness level of this scene.
|
||||
* @returns {number}
|
||||
*/
|
||||
get darknessLevel() {
|
||||
return this.#darknessLevel;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _draw(options) {
|
||||
await super._draw(options);
|
||||
this.#eventBoundary = new PIXI.EventBoundary(this);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Ambience Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the scene environment options.
|
||||
* @param {object} [config={}]
|
||||
* @param {ColorSource} [config.backgroundColor] The background canvas color
|
||||
* @param {ColorSource} [config.brightestColor] The brightest ambient color
|
||||
* @param {ColorSource} [config.darknessColor] The color of darkness
|
||||
* @param {ColorSource} [config.daylightColor] The ambient daylight color
|
||||
* @param {ColorSource} [config.fogExploredColor] The color applied to explored areas
|
||||
* @param {ColorSource} [config.fogUnexploredColor] The color applied to unexplored areas
|
||||
* @param {SceneEnvironmentData} [config.environment] The scene environment data
|
||||
* @fires PIXI.FederatedEvent type: "darknessChange" - event: {environmentData: {darknessLevel, priorDarknessLevel}}
|
||||
*/
|
||||
initialize({backgroundColor, brightestColor, darknessColor, daylightColor, fogExploredColor,
|
||||
fogUnexploredColor, darknessLevel, environment={}}={}) {
|
||||
const scene = canvas.scene;
|
||||
|
||||
// Update base ambient colors, and darkness level
|
||||
const fbc = EnvironmentCanvasGroup.#fallbackColors;
|
||||
this.colors.ambientDarkness = Color.from(darknessColor ?? CONFIG.Canvas.darknessColor ?? fbc.darknessColor);
|
||||
this.colors.ambientDaylight = Color.from(daylightColor
|
||||
?? (scene.tokenVision ? (CONFIG.Canvas.daylightColor ?? fbc.daylightColor) : 0xFFFFFF));
|
||||
this.colors.ambientBrightest = Color.from(brightestColor ?? CONFIG.Canvas.brightestColor ?? fbc.brightestColor);
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
*/
|
||||
if ( darknessLevel !== undefined ) {
|
||||
const msg = "config.darknessLevel parameter into EnvironmentCanvasGroup#initialize is deprecated. " +
|
||||
"You should pass the darkness level into config.environment.darknessLevel";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||||
environment.darknessLevel = darknessLevel;
|
||||
}
|
||||
|
||||
// Darkness Level Control
|
||||
const priorDarknessLevel = this.#darknessLevel ?? 0;
|
||||
const dl = environment.darknessLevel ?? scene.environment.darknessLevel;
|
||||
const darknessChanged = (dl !== this.#darknessLevel);
|
||||
this.#darknessLevel = scene.environment.darknessLevel = dl;
|
||||
|
||||
// Update weights
|
||||
Object.assign(this.weights, CONFIG.Canvas.lightLevels ?? {
|
||||
dark: 0,
|
||||
halfdark: 0.5,
|
||||
dim: 0.25,
|
||||
bright: 1
|
||||
});
|
||||
|
||||
// Compute colors
|
||||
this.#configureColors({fogExploredColor, fogUnexploredColor, backgroundColor});
|
||||
|
||||
// Configure the scene environment
|
||||
this.#configureEnvironment(environment);
|
||||
|
||||
// Update primary cached container and renderer clear color with scene background color
|
||||
canvas.app.renderer.background.color = this.colors.rendererBackground;
|
||||
canvas.primary._backgroundColor = this.colors.sceneBackground.rgb;
|
||||
|
||||
// Dispatching the darkness change event
|
||||
if ( darknessChanged ) {
|
||||
const event = new PIXI.FederatedEvent(this.#eventBoundary);
|
||||
event.type = "darknessChange";
|
||||
event.environmentData = {
|
||||
darknessLevel: this.#darknessLevel,
|
||||
priorDarknessLevel
|
||||
};
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Push a perception update to refresh lighting and sources with the new computed color values
|
||||
canvas.perception.update({
|
||||
refreshPrimary: true,
|
||||
refreshLighting: true,
|
||||
refreshVision: true
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure all colors pertaining to a scene.
|
||||
* @param {object} [options={}] Preview options.
|
||||
* @param {ColorSource} [options.fogExploredColor] A preview fog explored color.
|
||||
* @param {ColorSource} [options.fogUnexploredColor] A preview fog unexplored color.
|
||||
* @param {ColorSource} [options.backgroundColor] The background canvas color.
|
||||
*/
|
||||
#configureColors({fogExploredColor, fogUnexploredColor, backgroundColor}={}) {
|
||||
const scene = canvas.scene;
|
||||
const fbc = EnvironmentCanvasGroup.#fallbackColors;
|
||||
|
||||
// Compute the middle ambient color
|
||||
this.colors.background = this.colors.ambientDarkness.mix(this.colors.ambientDaylight, 1.0 - this.darknessLevel);
|
||||
|
||||
// Compute dark ambient colors
|
||||
this.colors.darkness = this.colors.ambientDarkness.mix(this.colors.background, this.weights.dark);
|
||||
this.colors.halfdark = this.colors.darkness.mix(this.colors.background, this.weights.halfdark);
|
||||
|
||||
// Compute light ambient colors
|
||||
this.colors.bright =
|
||||
this.colors.background.mix(this.colors.ambientBrightest, this.weights.bright);
|
||||
this.colors.dim = this.colors.background.mix(this.colors.bright, this.weights.dim);
|
||||
|
||||
// Compute fog colors
|
||||
const cfg = CONFIG.Canvas;
|
||||
const uc = Color.from(fogUnexploredColor ?? scene.fog.colors.unexplored ?? cfg.unexploredColor ?? fbc.fogUnexplored);
|
||||
this.colors.fogUnexplored = this.colors.background.multiply(uc);
|
||||
const ec = Color.from(fogExploredColor ?? scene.fog.colors.explored ?? cfg.exploredColor ?? fbc.fogExplored);
|
||||
this.colors.fogExplored = this.colors.background.multiply(ec);
|
||||
|
||||
// Compute scene background color
|
||||
const sceneBG = Color.from(backgroundColor ?? scene?.backgroundColor ?? fbc.backgroundColor);
|
||||
this.colors.sceneBackground = sceneBG;
|
||||
this.colors.rendererBackground = sceneBG.multiply(this.colors.background);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure the ambience filter for scene ambient lighting.
|
||||
* @param {SceneEnvironmentData} [environment] The scene environment data object.
|
||||
*/
|
||||
#configureEnvironment(environment={}) {
|
||||
const currentEnvironment = canvas.scene.toObject().environment;
|
||||
|
||||
/**
|
||||
* @type {SceneEnvironmentData}
|
||||
*/
|
||||
const data = foundry.utils.mergeObject(environment, currentEnvironment, {
|
||||
inplace: false,
|
||||
insertKeys: true,
|
||||
insertValues: true,
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
// First configure the ambience filter
|
||||
this.#configureAmbienceFilter(data);
|
||||
|
||||
// Then configure the global light
|
||||
this.#configureGlobalLight(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure the ambience filter.
|
||||
* @param {SceneEnvironmentData} environment
|
||||
* @param {boolean} environment.cycle The cycle option.
|
||||
* @param {EnvironmentData} environment.base The base environement data.
|
||||
* @param {EnvironmentData} environment.dark The dark environment data.
|
||||
*/
|
||||
#configureAmbienceFilter({cycle, base, dark}) {
|
||||
const ambienceFilter = canvas.primary._ambienceFilter;
|
||||
if ( !ambienceFilter ) return;
|
||||
const u = ambienceFilter.uniforms;
|
||||
|
||||
// Assigning base ambience parameters
|
||||
const bh = Color.fromHSL([base.hue, 1, 0.5]).linear;
|
||||
Color.applyRGB(bh, u.baseTint);
|
||||
u.baseLuminosity = base.luminosity;
|
||||
u.baseShadows = base.shadows;
|
||||
u.baseIntensity = base.intensity;
|
||||
u.baseSaturation = base.saturation;
|
||||
const baseAmbienceHasEffect = (base.luminosity !== 0) || (base.shadows > 0)
|
||||
|| (base.intensity > 0) || (base.saturation !== 0);
|
||||
|
||||
// Assigning dark ambience parameters
|
||||
const dh = Color.fromHSL([dark.hue, 1, 0.5]).linear;
|
||||
Color.applyRGB(dh, u.darkTint);
|
||||
u.darkLuminosity = dark.luminosity;
|
||||
u.darkShadows = dark.shadows;
|
||||
u.darkIntensity = dark.intensity;
|
||||
u.darkSaturation = dark.saturation;
|
||||
const darkAmbienceHasEffect = ((dark.luminosity !== 0) || (dark.shadows > 0)
|
||||
|| (dark.intensity > 0) || (dark.saturation !== 0)) && cycle;
|
||||
|
||||
// Assigning the cycle option
|
||||
u.cycle = cycle;
|
||||
|
||||
// Darkness level texture
|
||||
u.darknessLevelTexture = canvas.effects.illumination.renderTexture;
|
||||
|
||||
// Enable ambience filter if it is impacting visuals
|
||||
ambienceFilter.enabled = baseAmbienceHasEffect || darkAmbienceHasEffect;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure the global light.
|
||||
* @param {SceneEnvironmentData} environment
|
||||
* @param {GlobalLightData} environment.globalLight
|
||||
*/
|
||||
#configureGlobalLight({globalLight}) {
|
||||
const maxR = canvas.dimensions.maxR * 1.2;
|
||||
const globalLightData = foundry.utils.mergeObject({
|
||||
z: -Infinity,
|
||||
elevation: Infinity,
|
||||
dim: globalLight.bright ? 0 : maxR,
|
||||
bright: globalLight.bright ? maxR : 0,
|
||||
disabled: !globalLight.enabled
|
||||
}, globalLight, {overwrite: false});
|
||||
this.globalLightSource.initialize(globalLightData);
|
||||
this.globalLightSource.add();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get darknessPenalty() {
|
||||
const msg = "EnvironmentCanvasGroup#darknessPenalty is deprecated without replacement. " +
|
||||
"The darkness penalty is no longer applied on light and vision sources.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
116
resources/app/client/pixi/groups/hidden.js
Normal file
116
resources/app/client/pixi/groups/hidden.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* A specialized canvas group for rendering hidden containers before all others (like masks).
|
||||
* @extends {PIXI.Container}
|
||||
*/
|
||||
class HiddenCanvasGroup extends CanvasGroupMixin(PIXI.Container) {
|
||||
constructor() {
|
||||
super();
|
||||
this.eventMode = "none";
|
||||
this.#createMasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* The container which hold masks.
|
||||
* @type {PIXI.Container}
|
||||
*/
|
||||
masks = new PIXI.Container();
|
||||
|
||||
/** @override */
|
||||
static groupName = "hidden";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a mask to this group.
|
||||
* @param {string} name Name of the mask.
|
||||
* @param {PIXI.DisplayObject} displayObject Display object to add.
|
||||
* @param {number|undefined} [position=undefined] Position of the mask.
|
||||
*/
|
||||
addMask(name, displayObject, position) {
|
||||
if ( !((typeof name === "string") && (name.length > 0)) ) {
|
||||
throw new Error(`Adding mask failed. Name ${name} is invalid.`);
|
||||
}
|
||||
if ( !displayObject.clear ) {
|
||||
throw new Error("A mask container must implement a clear method.");
|
||||
}
|
||||
// Add the mask to the dedicated `masks` container
|
||||
this.masks[name] = position
|
||||
? this.masks.addChildAt(displayObject, position)
|
||||
: this.masks.addChild(displayObject);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Invalidate the masks: flag them for rerendering.
|
||||
*/
|
||||
invalidateMasks() {
|
||||
for ( const mask of this.masks.children ) {
|
||||
if ( !(mask instanceof CachedContainer) ) continue;
|
||||
mask.renderDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _draw(options) {
|
||||
this.invalidateMasks();
|
||||
this.addChild(this.masks);
|
||||
await this.#drawMasks();
|
||||
await super._draw(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Perform necessary draw operations.
|
||||
*/
|
||||
async #drawMasks() {
|
||||
await this.masks.vision.draw();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Attach masks container to this canvas layer and create tile occlusion, vision masks and depth mask.
|
||||
*/
|
||||
#createMasks() {
|
||||
// The canvas scissor mask is the first thing to render
|
||||
const canvas = new PIXI.LegacyGraphics();
|
||||
this.addMask("canvas", canvas);
|
||||
|
||||
// The scene scissor mask
|
||||
const scene = new PIXI.LegacyGraphics();
|
||||
this.addMask("scene", scene);
|
||||
|
||||
// Then we need to render vision mask
|
||||
const vision = new CanvasVisionMask();
|
||||
this.addMask("vision", vision);
|
||||
|
||||
// Then we need to render occlusion mask
|
||||
const occlusion = new CanvasOcclusionMask();
|
||||
this.addMask("occlusion", occlusion);
|
||||
|
||||
// Then the depth mask, which need occlusion
|
||||
const depth = new CanvasDepthMask();
|
||||
this.addMask("depth", depth);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tear-Down */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _tearDown(options) {
|
||||
this.removeChild(this.masks);
|
||||
|
||||
// Clear all masks (children of masks)
|
||||
this.masks.children.forEach(c => c.clear());
|
||||
|
||||
// Then proceed normally
|
||||
await super._tearDown(options);
|
||||
}
|
||||
}
|
||||
234
resources/app/client/pixi/groups/interface.js
Normal file
234
resources/app/client/pixi/groups/interface.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
13
resources/app/client/pixi/groups/overlay.js
Normal file
13
resources/app/client/pixi/groups/overlay.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A container group which is not bound to the stage world transform.
|
||||
*
|
||||
* @category - Canvas
|
||||
*/
|
||||
class OverlayCanvasGroup extends CanvasGroupMixin(UnboundContainer) {
|
||||
/** @override */
|
||||
static groupName = "overlay";
|
||||
|
||||
/** @override */
|
||||
static tearDownChildren = false;
|
||||
}
|
||||
|
||||
613
resources/app/client/pixi/groups/primary.js
Normal file
613
resources/app/client/pixi/groups/primary.js
Normal file
@@ -0,0 +1,613 @@
|
||||
/**
|
||||
* The primary Canvas group which generally contains tangible physical objects which exist within the Scene.
|
||||
* This group is a {@link CachedContainer} which is rendered to the Scene as a {@link SpriteMesh}.
|
||||
* This allows the rendered result of the Primary Canvas Group to be affected by a {@link BaseSamplerShader}.
|
||||
* @extends {CachedContainer}
|
||||
* @mixes CanvasGroupMixin
|
||||
* @category - Canvas
|
||||
*/
|
||||
class PrimaryCanvasGroup extends CanvasGroupMixin(CachedContainer) {
|
||||
constructor(sprite) {
|
||||
sprite ||= new SpriteMesh(undefined, BaseSamplerShader);
|
||||
super(sprite);
|
||||
this.eventMode = "none";
|
||||
this.#createAmbienceFilter();
|
||||
this.on("childAdded", this.#onChildAdded);
|
||||
this.on("childRemoved", this.#onChildRemoved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort order to break ties on the group/layer level.
|
||||
* @enum {number}
|
||||
*/
|
||||
static SORT_LAYERS = Object.freeze({
|
||||
SCENE: 0,
|
||||
TILES: 500,
|
||||
DRAWINGS: 600,
|
||||
TOKENS: 700,
|
||||
WEATHER: 1000
|
||||
});
|
||||
|
||||
/** @override */
|
||||
static groupName = "primary";
|
||||
|
||||
/** @override */
|
||||
static textureConfiguration = {
|
||||
scaleMode: PIXI.SCALE_MODES.NEAREST,
|
||||
format: PIXI.FORMATS.RGB,
|
||||
multisample: PIXI.MSAA_QUALITY.NONE
|
||||
};
|
||||
|
||||
/** @override */
|
||||
clearColor = [0, 0, 0, 0];
|
||||
|
||||
/**
|
||||
* The background color in RGB.
|
||||
* @type {[red: number, green: number, blue: number]}
|
||||
* @internal
|
||||
*/
|
||||
_backgroundColor;
|
||||
|
||||
/**
|
||||
* Track the set of HTMLVideoElements which are currently playing as part of this group.
|
||||
* @type {Set<SpriteMesh>}
|
||||
*/
|
||||
videoMeshes = new Set();
|
||||
|
||||
/**
|
||||
* Occludable objects above this elevation are faded on hover.
|
||||
* @type {number}
|
||||
*/
|
||||
hoverFadeElevation = 0;
|
||||
|
||||
/**
|
||||
* Allow API users to override the default elevation of the background layer.
|
||||
* This is a temporary solution until more formal support for scene levels is added in a future release.
|
||||
* @type {number}
|
||||
*/
|
||||
static BACKGROUND_ELEVATION = 0;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Group Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The primary background image configured for the Scene, rendered as a SpriteMesh.
|
||||
* @type {SpriteMesh}
|
||||
*/
|
||||
background;
|
||||
|
||||
/**
|
||||
* The primary foreground image configured for the Scene, rendered as a SpriteMesh.
|
||||
* @type {SpriteMesh}
|
||||
*/
|
||||
foreground;
|
||||
|
||||
/**
|
||||
* A Quadtree which partitions and organizes primary canvas objects.
|
||||
* @type {CanvasQuadtree}
|
||||
*/
|
||||
quadtree = new CanvasQuadtree();
|
||||
|
||||
/**
|
||||
* The collection of PrimaryDrawingContainer objects which are rendered in the Scene.
|
||||
* @type {Collection<string, PrimaryDrawingContainer>}
|
||||
*/
|
||||
drawings = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* The collection of SpriteMesh objects which are rendered in the Scene.
|
||||
* @type {Collection<string, TokenMesh>}
|
||||
*/
|
||||
tokens = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* The collection of SpriteMesh objects which are rendered in the Scene.
|
||||
* @type {Collection<string, PrimarySpriteMesh|TileSprite>}
|
||||
*/
|
||||
tiles = new foundry.utils.Collection();
|
||||
|
||||
/**
|
||||
* The ambience filter which is applying post-processing effects.
|
||||
* @type {PrimaryCanvasGroupAmbienceFilter}
|
||||
* @internal
|
||||
*/
|
||||
_ambienceFilter;
|
||||
|
||||
/**
|
||||
* The objects that are currently hovered in reverse sort order.
|
||||
* @type {PrimaryCanvasObjec[]>}
|
||||
*/
|
||||
#hoveredObjects = [];
|
||||
|
||||
/**
|
||||
* Trace the tiling sprite error to avoid multiple warning.
|
||||
* FIXME: Remove when the deprecation period for the tiling sprite error is over.
|
||||
* @type {boolean}
|
||||
* @internal
|
||||
*/
|
||||
#tilingSpriteError = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Group Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the base HTML image or video element which provides the background texture.
|
||||
* @type {HTMLImageElement|HTMLVideoElement}
|
||||
*/
|
||||
get backgroundSource() {
|
||||
if ( !this.background.texture.valid || this.background.texture === PIXI.Texture.WHITE ) return null;
|
||||
return this.background.texture.baseTexture.resource.source;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the base HTML image or video element which provides the foreground texture.
|
||||
* @type {HTMLImageElement|HTMLVideoElement}
|
||||
*/
|
||||
get foregroundSource() {
|
||||
if ( !this.foreground.texture.valid ) return null;
|
||||
return this.foreground.texture.baseTexture.resource.source;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the ambience filter bound to the primary group.
|
||||
*/
|
||||
#createAmbienceFilter() {
|
||||
if ( this._ambienceFilter ) this._ambienceFilter.enabled = false;
|
||||
else {
|
||||
this.filters ??= [];
|
||||
const f = this._ambienceFilter = PrimaryCanvasGroupAmbienceFilter.create();
|
||||
f.enabled = false;
|
||||
this.filterArea = canvas.app.renderer.screen;
|
||||
this.filters.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the primary mesh.
|
||||
*/
|
||||
refreshPrimarySpriteMesh() {
|
||||
const singleSource = canvas.visibility.visionModeData.source;
|
||||
const vmOptions = singleSource?.visionMode.canvas;
|
||||
const isBaseSampler = (this.sprite.shader.constructor === BaseSamplerShader);
|
||||
if ( !vmOptions && isBaseSampler ) return;
|
||||
|
||||
// Update the primary sprite shader class (or reset to BaseSamplerShader)
|
||||
this.sprite.setShaderClass(vmOptions?.shader ?? BaseSamplerShader);
|
||||
this.sprite.shader.uniforms.sampler = this.renderTexture;
|
||||
|
||||
// Need to update uniforms?
|
||||
if ( !vmOptions?.uniforms ) return;
|
||||
vmOptions.uniforms.linkedToDarknessLevel = singleSource?.visionMode.vision.darkness.adaptive;
|
||||
vmOptions.uniforms.darknessLevel = canvas.environment.darknessLevel;
|
||||
vmOptions.uniforms.darknessLevelTexture = canvas.effects.illumination.renderTexture;
|
||||
vmOptions.uniforms.screenDimensions = canvas.screenDimensions;
|
||||
|
||||
// Assigning color from source if any
|
||||
vmOptions.uniforms.tint = singleSource?.visionModeOverrides.colorRGB
|
||||
?? this.sprite.shader.constructor.defaultUniforms.tint;
|
||||
|
||||
// Updating uniforms in the primary sprite shader
|
||||
for ( const [uniform, value] of Object.entries(vmOptions?.uniforms ?? {}) ) {
|
||||
if ( uniform in this.sprite.shader.uniforms ) this.sprite.shader.uniforms[uniform] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update this group. Calculates the canvas transform and bounds of all its children and updates the quadtree.
|
||||
*/
|
||||
update() {
|
||||
if ( this.sortDirty ) this.sortChildren();
|
||||
const children = this.children;
|
||||
for ( let i = 0, n = children.length; i < n; i++ ) {
|
||||
children[i].updateCanvasTransform?.();
|
||||
}
|
||||
canvas.masks.depth._update();
|
||||
if ( !CONFIG.debug.canvas.primary.bounds ) return;
|
||||
const dbg = canvas.controls.debug.clear().lineStyle(5, 0x30FF00);
|
||||
for ( const child of this.children ) {
|
||||
if ( child.canvasBounds ) dbg.drawShape(child.canvasBounds);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _draw(options) {
|
||||
this.#drawBackground();
|
||||
this.#drawForeground();
|
||||
this.#drawPadding();
|
||||
this.hoverFadeElevation = 0;
|
||||
await super._draw(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_render(renderer) {
|
||||
const [r, g, b] = this._backgroundColor;
|
||||
renderer.framebuffer.clear(r, g, b, 1, PIXI.BUFFER_BITS.COLOR);
|
||||
super._render(renderer);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the Scene background image.
|
||||
*/
|
||||
#drawBackground() {
|
||||
const bg = this.background = this.addChild(new PrimarySpriteMesh({name: "background", object: this}));
|
||||
bg.elevation = this.constructor.BACKGROUND_ELEVATION;
|
||||
const bgTextureSrc = canvas.sceneTextures.background ?? canvas.scene.background.src;
|
||||
const bgTexture = bgTextureSrc instanceof PIXI.Texture ? bgTextureSrc : getTexture(bgTextureSrc);
|
||||
this.#drawSceneMesh(bg, bgTexture);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the Scene foreground image.
|
||||
*/
|
||||
#drawForeground() {
|
||||
const fg = this.foreground = this.addChild(new PrimarySpriteMesh({name: "foreground", object: this}));
|
||||
fg.elevation = canvas.scene.foregroundElevation;
|
||||
const fgTextureSrc = canvas.sceneTextures.foreground ?? canvas.scene.foreground;
|
||||
const fgTexture = fgTextureSrc instanceof PIXI.Texture ? fgTextureSrc : getTexture(fgTextureSrc);
|
||||
|
||||
// Compare dimensions with background texture and draw the mesh
|
||||
const bg = this.background.texture;
|
||||
if ( fgTexture && bg && ((fgTexture.width !== bg.width) || (fgTexture.height !== bg.height)) ) {
|
||||
ui.notifications.warn("WARNING.ForegroundDimensionsMismatch", {localize: true});
|
||||
}
|
||||
this.#drawSceneMesh(fg, fgTexture);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw a PrimarySpriteMesh that fills the entire Scene rectangle.
|
||||
* @param {PrimarySpriteMesh} mesh The target PrimarySpriteMesh
|
||||
* @param {PIXI.Texture|null} texture The loaded Texture or null
|
||||
*/
|
||||
#drawSceneMesh(mesh, texture) {
|
||||
const d = canvas.dimensions;
|
||||
mesh.texture = texture ?? PIXI.Texture.EMPTY;
|
||||
mesh.textureAlphaThreshold = 0.75;
|
||||
mesh.occludedAlpha = 0.5;
|
||||
mesh.visible = mesh.texture !== PIXI.Texture.EMPTY;
|
||||
mesh.position.set(d.sceneX, d.sceneY);
|
||||
mesh.width = d.sceneWidth;
|
||||
mesh.height = d.sceneHeight;
|
||||
mesh.sortLayer = PrimaryCanvasGroup.SORT_LAYERS.SCENE;
|
||||
mesh.zIndex = -Infinity;
|
||||
mesh.hoverFade = false;
|
||||
|
||||
// Manage video playback
|
||||
const video = game.video.getVideoSource(mesh);
|
||||
if ( video ) {
|
||||
this.videoMeshes.add(mesh);
|
||||
game.video.play(video, {volume: game.settings.get("core", "globalAmbientVolume")});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the Scene padding.
|
||||
*/
|
||||
#drawPadding() {
|
||||
const d = canvas.dimensions;
|
||||
const g = this.addChild(new PIXI.LegacyGraphics());
|
||||
g.beginFill(0x000000, 0.025)
|
||||
.drawShape(d.rect)
|
||||
.beginHole()
|
||||
.drawShape(d.sceneRect)
|
||||
.endHole()
|
||||
.endFill();
|
||||
g.elevation = -Infinity;
|
||||
g.sort = -Infinity;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tear-Down */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _tearDown(options) {
|
||||
|
||||
// Stop video playback
|
||||
for ( const mesh of this.videoMeshes ) game.video.stop(mesh.sourceElement);
|
||||
|
||||
await super._tearDown(options);
|
||||
|
||||
// Clear collections
|
||||
this.videoMeshes.clear();
|
||||
this.tokens.clear();
|
||||
this.tiles.clear();
|
||||
|
||||
// Clear the quadtree
|
||||
this.quadtree.clear();
|
||||
|
||||
// Reset the tiling sprite tracker
|
||||
this.#tilingSpriteError = false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Token Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the SpriteMesh for a specific Token object.
|
||||
* @param {Token} token The Token being added
|
||||
* @returns {PrimarySpriteMesh} The added PrimarySpriteMesh
|
||||
*/
|
||||
addToken(token) {
|
||||
const name = token.objectId;
|
||||
|
||||
// Create the token mesh
|
||||
const mesh = this.tokens.get(name) ?? this.addChild(new PrimarySpriteMesh({name, object: token}));
|
||||
mesh.texture = token.texture ?? PIXI.Texture.EMPTY;
|
||||
this.tokens.set(name, mesh);
|
||||
if ( mesh.isVideo ) this.videoMeshes.add(mesh);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove a TokenMesh from the group.
|
||||
* @param {Token} token The Token being removed
|
||||
*/
|
||||
removeToken(token) {
|
||||
const name = token.objectId;
|
||||
const mesh = this.tokens.get(name);
|
||||
if ( mesh?.destroyed === false ) mesh.destroy({children: true});
|
||||
this.tokens.delete(name);
|
||||
this.videoMeshes.delete(mesh);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tile Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the SpriteMesh for a specific Token object.
|
||||
* @param {Tile} tile The Tile being added
|
||||
* @returns {PrimarySpriteMesh} The added PrimarySpriteMesh
|
||||
*/
|
||||
addTile(tile) {
|
||||
/** @deprecated since v12 */
|
||||
if ( !this.#tilingSpriteError && tile.document.getFlag("core", "isTilingSprite") ) {
|
||||
this.#tilingSpriteError = true;
|
||||
ui.notifications.warn("WARNING.TilingSpriteDeprecation", {localize: true, permanent: true});
|
||||
const msg = "Tiling Sprites are deprecated without replacement.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
}
|
||||
|
||||
const name = tile.objectId;
|
||||
let mesh = this.tiles.get(name) ?? this.addChild(new PrimarySpriteMesh({name, object: tile}));
|
||||
mesh.texture = tile.texture ?? PIXI.Texture.EMPTY;
|
||||
this.tiles.set(name, mesh);
|
||||
if ( mesh.isVideo ) this.videoMeshes.add(mesh);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove a TokenMesh from the group.
|
||||
* @param {Tile} tile The Tile being removed
|
||||
*/
|
||||
removeTile(tile) {
|
||||
const name = tile.objectId;
|
||||
const mesh = this.tiles.get(name);
|
||||
if ( mesh?.destroyed === false ) mesh.destroy({children: true});
|
||||
this.tiles.delete(name);
|
||||
this.videoMeshes.delete(mesh);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Drawing Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a PrimaryGraphics to the group.
|
||||
* @param {Drawing} drawing The Drawing being added
|
||||
* @returns {PrimaryGraphics} The created PrimaryGraphics instance
|
||||
*/
|
||||
addDrawing(drawing) {
|
||||
const name = drawing.objectId;
|
||||
const shape = this.drawings.get(name) ?? this.addChild(new PrimaryGraphics({name, object: drawing}));
|
||||
this.drawings.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.has(name) ) return;
|
||||
const shape = this.drawings.get(name);
|
||||
if ( shape?.destroyed === false ) shape.destroy({children: true});
|
||||
this.drawings.delete(name);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Override the default PIXI.Container behavior for how objects in this container are sorted.
|
||||
* @override
|
||||
*/
|
||||
sortChildren() {
|
||||
const children = this.children;
|
||||
for ( let i = 0, n = children.length; i < n; i++ ) children[i]._lastSortedIndex = i;
|
||||
children.sort(PrimaryCanvasGroup.#compareObjects);
|
||||
this.sortDirty = false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The sorting function used to order objects inside the Primary Canvas Group.
|
||||
* Overrides the default sorting function defined for the PIXI.Container.
|
||||
* Sort Tokens PCO above other objects except WeatherEffects, then Drawings PCO, all else held equal.
|
||||
* @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.sortLayer || 0) - (b.sortLayer || 0))
|
||||
|| ((a.sort || 0) - (b.sort || 0))
|
||||
|| (a.zIndex - b.zIndex)
|
||||
|| (a._lastSortedIndex - b._lastSortedIndex);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* PIXI Events */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Called when a child is added.
|
||||
* @param {PIXI.DisplayObject} child
|
||||
*/
|
||||
#onChildAdded(child) {
|
||||
if ( child.shouldRenderDepth ) canvas.masks.depth._elevationDirty = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Called when a child is removed.
|
||||
* @param {PIXI.DisplayObject} child
|
||||
*/
|
||||
#onChildRemoved(child) {
|
||||
if ( child.shouldRenderDepth ) canvas.masks.depth._elevationDirty = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mousemove events on the primary group to update the hovered state of its children.
|
||||
* @internal
|
||||
*/
|
||||
_onMouseMove() {
|
||||
const time = canvas.app.ticker.lastTime;
|
||||
|
||||
// Unset the hovered state of the hovered PCOs
|
||||
for ( const object of this.#hoveredObjects ) {
|
||||
if ( !object._hoverFadeState.hovered ) continue;
|
||||
object._hoverFadeState.hovered = false;
|
||||
object._hoverFadeState.hoveredTime = time;
|
||||
}
|
||||
|
||||
this.#updateHoveredObjects();
|
||||
|
||||
// Set the hovered state of the hovered PCOs
|
||||
for ( const object of this.#hoveredObjects ) {
|
||||
if ( !object.hoverFade || !(object.elevation > this.hoverFadeElevation) ) break;
|
||||
object._hoverFadeState.hovered = true;
|
||||
object._hoverFadeState.hoveredTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the hovered objects. Returns the hovered objects.
|
||||
*/
|
||||
#updateHoveredObjects() {
|
||||
this.#hoveredObjects.length = 0;
|
||||
|
||||
// Get all PCOs that contain the mouse position
|
||||
const position = canvas.mousePosition;
|
||||
const collisionTest = ({t}) => t.visible && t.renderable
|
||||
&& t._hoverFadeState && t.containsCanvasPoint(position);
|
||||
for ( const object of canvas.primary.quadtree.getObjects(
|
||||
new PIXI.Rectangle(position.x, position.y, 0, 0), {collisionTest}
|
||||
)) {
|
||||
this.#hoveredObjects.push(object);
|
||||
}
|
||||
|
||||
// Sort the hovered PCOs in reverse primary order
|
||||
this.#hoveredObjects.sort((a, b) => PrimaryCanvasGroup.#compareObjects(b, a));
|
||||
|
||||
// Discard hit objects below the hovered placeable
|
||||
const hoveredPlaceable = canvas.activeLayer?.hover;
|
||||
if ( hoveredPlaceable ) {
|
||||
let elevation = 0;
|
||||
let sortLayer = Infinity;
|
||||
let sort = Infinity;
|
||||
let zIndex = Infinity;
|
||||
if ( (hoveredPlaceable instanceof Token) || (hoveredPlaceable instanceof Tile) ) {
|
||||
const mesh = hoveredPlaceable.mesh;
|
||||
if ( mesh ) {
|
||||
elevation = mesh.elevation;
|
||||
sortLayer = mesh.sortLayer;
|
||||
sort = mesh.sort;
|
||||
zIndex = mesh.zIndex;
|
||||
}
|
||||
} else if ( hoveredPlaceable instanceof Drawing ) {
|
||||
const shape = hoveredPlaceable.shape;
|
||||
if ( shape ) {
|
||||
elevation = shape.elevation;
|
||||
sortLayer = shape.sortLayer;
|
||||
sort = shape.sort;
|
||||
zIndex = shape.zIndex;
|
||||
}
|
||||
} else if ( hoveredPlaceable.document.schema.has("elevation") ) {
|
||||
elevation = hoveredPlaceable.document.elevation;
|
||||
}
|
||||
const threshold = {elevation, sortLayer, sort, zIndex, _lastSortedIndex: Infinity};
|
||||
while ( this.#hoveredObjects.length
|
||||
&& PrimaryCanvasGroup.#compareObjects(this.#hoveredObjects.at(-1), threshold) <= 0 ) {
|
||||
this.#hoveredObjects.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
mapElevationToDepth(elevation) {
|
||||
const msg = "PrimaryCanvasGroup#mapElevationAlpha is deprecated. "
|
||||
+ "Use canvas.masks.depth.mapElevation(elevation) instead.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return canvas.masks.depth.mapElevation(elevation);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
mapElevationAlpha(elevation) {
|
||||
const msg = "PrimaryCanvasGroup#mapElevationAlpha is deprecated. "
|
||||
+ "Use canvas.masks.depth.mapElevation(elevation) instead.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
|
||||
return canvas.masks.depth.mapElevation(elevation);
|
||||
}
|
||||
}
|
||||
13
resources/app/client/pixi/groups/rendered.js
Normal file
13
resources/app/client/pixi/groups/rendered.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A container group which contains the environment canvas group and the interface canvas group.
|
||||
*
|
||||
* @category - Canvas
|
||||
*/
|
||||
class RenderedCanvasGroup extends CanvasGroupMixin(PIXI.Container) {
|
||||
/** @override */
|
||||
static groupName = "rendered";
|
||||
|
||||
/** @override */
|
||||
static tearDownChildren = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user