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

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

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

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

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

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

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