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