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,126 @@
/**
* The depth mask which contains a mapping of elevation. Needed to know if we must render objects according to depth.
* Red channel: Lighting occlusion (top).
* Green channel: Lighting occlusion (bottom).
* Blue channel: Weather occlusion.
* @category - Canvas
*/
class CanvasDepthMask extends CachedContainer {
constructor(...args) {
super(...args);
this.#createDepth();
}
/**
* Container in which roofs are rendered with depth data.
* @type {PIXI.Container}
*/
roofs;
/** @override */
static textureConfiguration = {
scaleMode: PIXI.SCALE_MODES.NEAREST,
format: PIXI.FORMATS.RGB,
multisample: PIXI.MSAA_QUALITY.NONE
};
/** @override */
clearColor = [0, 0, 0, 0];
/**
* Update the elevation-to-depth mapping?
* @type {boolean}
* @internal
*/
_elevationDirty = false;
/**
* The elevations of the elevation-to-depth mapping.
* Supported are up to 255 unique elevations.
* @type {Float64Array}
*/
#elevations = new Float64Array([-Infinity]);
/* -------------------------------------------- */
/**
* Map an elevation to a value in the range [0, 1] with 8-bit precision.
* The depth-rendered object are rendered with these values into the render texture.
* @param {number} elevation The elevation in distance units
* @returns {number} The value for this elevation in the range [0, 1] with 8-bit precision
*/
mapElevation(elevation) {
const E = this.#elevations;
if ( elevation < E[0] ) return 0;
let i = 0;
let j = E.length - 1;
while ( i < j ) {
const k = (i + j + 1) >> 1;
const e = E[k];
if ( e <= elevation ) i = k;
else j = k - 1;
}
return (i + 1) / 255;
}
/* -------------------------------------------- */
/**
* Update the elevation-to-depth mapping.
* Needs to be called after the children have been sorted
* and the canvas transform phase.
* @internal
*/
_update() {
if ( !this._elevationDirty ) return;
this._elevationDirty = false;
const elevations = [];
const children = canvas.primary.children;
for ( let i = 0, n = children.length; i < n; i++ ) {
const child = children[i];
if ( !child.shouldRenderDepth ) continue;
const elevation = child.elevation;
if ( elevation === elevations.at(-1) ) continue;
elevations.push(elevation);
}
if ( !elevations.length ) elevations.push(-Infinity);
else elevations.length = Math.min(elevations.length, 255);
this.#elevations = new Float64Array(elevations);
}
/* -------------------------------------------- */
/**
* Initialize the depth mask with the roofs container and token graphics.
*/
#createDepth() {
this.roofs = this.addChild(this.#createRoofsContainer());
}
/* -------------------------------------------- */
/**
* Create the roofs container.
* @returns {PIXI.Container}
*/
#createRoofsContainer() {
const c = new PIXI.Container();
const render = renderer => {
// Render the depth of each primary canvas object
for ( const pco of canvas.primary.children ) {
pco.renderDepthData?.(renderer);
}
};
c.render = render.bind(c);
return c;
}
/* -------------------------------------------- */
/**
* Clear the depth mask.
*/
clear() {
Canvas.clearContainer(this.roofs, false);
}
}

View File

@@ -0,0 +1,204 @@
/**
* The occlusion mask which contains radial occlusion and vision occlusion from tokens.
* Red channel: Fade occlusion.
* Green channel: Radial occlusion.
* Blue channel: Vision occlusion.
* @category - Canvas
*/
class CanvasOcclusionMask extends CachedContainer {
constructor(...args) {
super(...args);
this.#createOcclusion();
}
/** @override */
static textureConfiguration = {
scaleMode: PIXI.SCALE_MODES.NEAREST,
format: PIXI.FORMATS.RGB,
multisample: PIXI.MSAA_QUALITY.NONE
};
/**
* Graphics in which token radial and vision occlusion shapes are drawn.
* @type {PIXI.LegacyGraphics}
*/
tokens;
/**
* The occludable tokens.
* @type {Token[]}
*/
#tokens;
/** @override */
clearColor = [0, 1, 1, 1];
/** @override */
autoRender = false;
/* -------------------------------------------- */
/**
* Is vision occlusion active?
* @type {boolean}
*/
get vision() {
return this.#vision;
}
/**
* @type {boolean}
*/
#vision = false;
/**
* The elevations of the elevation-to-depth mapping.
* Supported are up to 255 unique elevations.
* @type {Float64Array}
*/
#elevations = new Float64Array([-Infinity]);
/* -------------------------------------------- */
/**
* Initialize the depth mask with the roofs container and token graphics.
*/
#createOcclusion() {
this.alphaMode = PIXI.ALPHA_MODES.NO_PREMULTIPLIED_ALPHA;
this.tokens = this.addChild(new PIXI.LegacyGraphics());
this.tokens.blendMode = PIXI.BLEND_MODES.MIN_ALL;
}
/* -------------------------------------------- */
/**
* Clear the occlusion mask.
*/
clear() {
this.tokens.clear();
}
/* -------------------------------------------- */
/* Occlusion Management */
/* -------------------------------------------- */
/**
* Map an elevation to a value in the range [0, 1] with 8-bit precision.
* The radial and vision shapes are drawn with these values into the render texture.
* @param {number} elevation The elevation in distance units
* @returns {number} The value for this elevation in the range [0, 1] with 8-bit precision
*/
mapElevation(elevation) {
const E = this.#elevations;
let i = 0;
let j = E.length - 1;
if ( elevation > E[j] ) return 1;
while ( i < j ) {
const k = (i + j) >> 1;
const e = E[k];
if ( e >= elevation ) j = k;
else i = k + 1;
}
return i / 255;
}
/* -------------------------------------------- */
/**
* Update the set of occludable Tokens, redraw the occlusion mask, and update the occluded state
* of all occludable objects.
*/
updateOcclusion() {
this.#tokens = canvas.tokens._getOccludableTokens();
this._updateOcclusionMask();
this._updateOcclusionStates();
}
/* -------------------------------------------- */
/**
* Draw occlusion shapes to the occlusion mask.
* Fade occlusion draws to the red channel with varying intensity from [0, 1] based on elevation.
* Radial occlusion draws to the green channel with varying intensity from [0, 1] based on elevation.
* Vision occlusion draws to the blue channel with varying intensity from [0, 1] based on elevation.
* @internal
*/
_updateOcclusionMask() {
this.#vision = false;
this.tokens.clear();
const elevations = [];
for ( const token of this.#tokens.sort((a, b) => a.document.elevation - b.document.elevation) ) {
const elevation = token.document.elevation;
if ( elevation !== elevations.at(-1) ) elevations.push(elevation);
const occlusionElevation = Math.min(elevations.length - 1, 255);
// Draw vision occlusion
if ( token.vision?.active ) {
this.#vision = true;
this.tokens.beginFill(0xFFFF00 | occlusionElevation).drawShape(token.vision.los).endFill();
}
// Draw radial occlusion (and radial into the vision channel if this token doesn't have vision)
const origin = token.center;
const occlusionRadius = Math.max(token.externalRadius, token.getLightRadius(token.document.occludable.radius));
this.tokens.beginFill(0xFF0000 | (occlusionElevation << 8) | (token.vision?.active ? 0xFF : occlusionElevation))
.drawCircle(origin.x, origin.y, occlusionRadius).endFill();
}
if ( !elevations.length ) elevations.push(-Infinity);
else elevations.length = Math.min(elevations.length, 255);
this.#elevations = new Float64Array(elevations);
this.renderDirty = true;
}
/* -------------------------------------------- */
/**
* Update the current occlusion status of all Tile objects.
* @internal
*/
_updateOcclusionStates() {
const occluded = this._identifyOccludedObjects(this.#tokens);
for ( const pco of canvas.primary.children ) {
const isOccludable = pco.isOccludable;
if ( (isOccludable === undefined) || (!isOccludable && !pco.occluded) ) continue;
pco.debounceSetOcclusion(occluded.has(pco));
}
}
/* -------------------------------------------- */
/**
* Determine the set of objects which should be currently occluded by a Token.
* @param {Token[]} tokens The set of currently controlled Token objects
* @returns {Set<PrimaryCanvasObjectMixin>} The PCO objects which should be currently occluded
* @protected
*/
_identifyOccludedObjects(tokens) {
const occluded = new Set();
for ( const token of tokens ) {
// Get the occludable primary canvas objects (PCO) according to the token bounds
const matchingPCO = canvas.primary.quadtree.getObjects(token.bounds);
for ( const pco of matchingPCO ) {
// Don't bother re-testing a PCO or an object which is not occludable
if ( !pco.isOccludable || occluded.has(pco) ) continue;
if ( pco.testOcclusion(token, {corners: pco.restrictsLight && pco.restrictsWeather}) ) occluded.add(pco);
}
}
return occluded;
}
/* -------------------------------------------- */
/* Deprecation and compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
_identifyOccludedTiles() {
const msg = "CanvasOcclusionMask#_identifyOccludedTiles has been deprecated in " +
"favor of CanvasOcclusionMask#_identifyOccludedObjects.";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
return this._identifyOccludedObjects();
}
}

View File

@@ -0,0 +1,162 @@
/**
* @typedef {object} _CanvasVisionContainerSight
* @property {PIXI.LegacyGraphics} preview FOV that should not be committed to fog exploration.
*/
/**
* The sight part of {@link CanvasVisionContainer}.
* The blend mode is MAX_COLOR.
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerSight} CanvasVisionContainerSight
*/
/**
* @typedef {object} _CanvasVisionContainerLight
* @property {PIXI.LegacyGraphics} preview FOV that should not be committed to fog exploration.
* @property {SpriteMesh} cached The sprite with the texture of FOV of cached light sources.
* @property {PIXI.LegacyGraphics & {preview: PIXI.LegacyGraphics}} mask
* The light perception polygons of vision sources and the FOV of vision sources that provide vision.
*/
/**
* The light part of {@link CanvasVisionContainer}.
* The blend mode is MAX_COLOR.
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerLight} CanvasVisionContainerLight
*/
/**
* @typedef {object} _CanvasVisionContainerDarkness
* @property {PIXI.LegacyGraphics} darkness Darkness source erasing fog of war.
*/
/**
* The sight part of {@link CanvasVisionContainer}.
* The blend mode is ERASE.
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerDarkness} CanvasVisionContainerDarkness
*/
/**
* The sight part of {@link CanvasVisionContainer}.
* The blend mode is MAX_COLOR.
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerSight} CanvasVisionContainerSight
*/
/**
* @typedef {object} _CanvasVisionContainer
* @property {CanvasVisionContainerLight} light Areas visible because of light sources and light perception.
* @property {CanvasVisionContainerSight} sight Areas visible because of FOV of vision sources.
* @property {CanvasVisionContainerDarkness} darkness Areas erased by darkness sources.
*/
/**
* The currently visible areas.
* @typedef {PIXI.Container & _CanvasVisionContainer} CanvasVisionContainer
*/
/**
* The vision mask which contains the current line-of-sight texture.
* @category - Canvas
*/
class CanvasVisionMask extends CachedContainer {
/** @override */
static textureConfiguration = {
scaleMode: PIXI.SCALE_MODES.NEAREST,
format: PIXI.FORMATS.RED,
multisample: PIXI.MSAA_QUALITY.NONE
};
/** @override */
clearColor = [0, 0, 0, 0];
/** @override */
autoRender = false;
/**
* The current vision Container.
* @type {CanvasVisionContainer}
*/
vision;
/**
* The BlurFilter which applies to the vision mask texture.
* This filter applies a NORMAL blend mode to the container.
* @type {AlphaBlurFilter}
*/
blurFilter;
/* -------------------------------------------- */
/**
* Create the BlurFilter for the VisionMask container.
* @returns {AlphaBlurFilter}
*/
#createBlurFilter() {
// Initialize filters properties
this.filters ??= [];
this.filterArea = null;
// Check if the canvas blur is disabled and return without doing anything if necessary
const b = canvas.blur;
this.filters.findSplice(f => f === this.blurFilter);
if ( !b.enabled ) return;
// Create the new filter
const f = this.blurFilter = new b.blurClass(b.strength, b.passes, PIXI.Filter.defaultResolution, b.kernels);
f.blendMode = PIXI.BLEND_MODES.NORMAL;
this.filterArea = canvas.app.renderer.screen;
this.filters.push(f);
return canvas.addBlurFilter(this.blurFilter);
}
/* -------------------------------------------- */
async draw() {
this.#createBlurFilter();
}
/* -------------------------------------------- */
/**
* Initialize the vision mask with the los and the fov graphics objects.
* @param {PIXI.Container} vision The vision container to attach
* @returns {CanvasVisionContainer}
*/
attachVision(vision) {
return this.vision = this.addChild(vision);
}
/* -------------------------------------------- */
/**
* Detach the vision mask from the cached container.
* @returns {CanvasVisionContainer} The detached vision container.
*/
detachVision() {
const vision = this.vision;
this.removeChild(vision);
this.vision = undefined;
return vision;
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
get filter() {
foundry.utils.logCompatibilityWarning("CanvasVisionMask#filter has been renamed to blurFilter.", {since: 11, until: 13});
return this.blurFilter;
}
/**
* @deprecated since v11
* @ignore
*/
set filter(f) {
foundry.utils.logCompatibilityWarning("CanvasVisionMask#filter has been renamed to blurFilter.", {since: 11, until: 13});
this.blurFilter = f;
}
}