Initial
This commit is contained in:
482
resources/app/client/pixi/placeables/light.js
Normal file
482
resources/app/client/pixi/placeables/light.js
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* An AmbientLight is an implementation of PlaceableObject which represents a dynamic light source within the Scene.
|
||||
* @category - Canvas
|
||||
* @see {@link AmbientLightDocument}
|
||||
* @see {@link LightingLayer}
|
||||
*/
|
||||
class AmbientLight extends PlaceableObject {
|
||||
/**
|
||||
* The area that is affected by this light.
|
||||
* @type {PIXI.Graphics}
|
||||
*/
|
||||
field;
|
||||
|
||||
/**
|
||||
* A reference to the PointSource object which defines this light or darkness area of effect.
|
||||
* This is undefined if the AmbientLight does not provide an active source of light.
|
||||
* @type {PointDarknessSource|PointLightSource}
|
||||
*/
|
||||
lightSource;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static embeddedName = "AmbientLight";
|
||||
|
||||
/** @override */
|
||||
static RENDER_FLAGS = {
|
||||
redraw: {propagate: ["refresh"]},
|
||||
refresh: {propagate: ["refreshState", "refreshField", "refreshElevation"], alias: true},
|
||||
refreshField: {propagate: ["refreshPosition"]},
|
||||
refreshPosition: {},
|
||||
refreshState: {},
|
||||
refreshElevation: {}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get bounds() {
|
||||
const {x, y} = this.document;
|
||||
const r = Math.max(this.dimRadius, this.brightRadius);
|
||||
return new PIXI.Rectangle(x-r, y-r, 2*r, 2*r);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get sourceId() {
|
||||
let id = `${this.document.documentName}.${this.document.id}`;
|
||||
if ( this.isPreview ) id += ".preview";
|
||||
return id;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A convenience accessor to the LightData configuration object
|
||||
* @returns {LightData}
|
||||
*/
|
||||
get config() {
|
||||
return this.document.config;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether a specific AmbientLight source provides global illumination
|
||||
* @type {boolean}
|
||||
*/
|
||||
get global() {
|
||||
return this.document.isGlobal;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The maximum radius in pixels of the light field
|
||||
* @type {number}
|
||||
*/
|
||||
get radius() {
|
||||
return Math.max(Math.abs(this.dimRadius), Math.abs(this.brightRadius));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the pixel radius of dim light emitted by this light source
|
||||
* @type {number}
|
||||
*/
|
||||
get dimRadius() {
|
||||
let d = canvas.dimensions;
|
||||
return ((this.config.dim / d.distance) * d.size);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the pixel radius of bright light emitted by this light source
|
||||
* @type {number}
|
||||
*/
|
||||
get brightRadius() {
|
||||
let d = canvas.dimensions;
|
||||
return ((this.config.bright / d.distance) * d.size);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this Ambient Light currently visible? By default, true only if the source actively emits light or darkness.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isVisible() {
|
||||
return !this._isLightSourceDisabled();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check if the point source is a LightSource instance
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isLightSource() {
|
||||
return this.lightSource instanceof CONFIG.Canvas.lightSourceClass;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check if the point source is a DarknessSource instance
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isDarknessSource() {
|
||||
return this.lightSource instanceof CONFIG.Canvas.darknessSourceClass;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is the source of this Ambient Light disabled?
|
||||
* @type {boolean}
|
||||
* @protected
|
||||
*/
|
||||
_isLightSourceDisabled() {
|
||||
const {hidden, config} = this.document;
|
||||
|
||||
// Hidden lights are disabled
|
||||
if ( hidden ) return true;
|
||||
|
||||
// Lights with zero radius or angle are disabled
|
||||
if ( !(this.radius && config.angle) ) return true;
|
||||
|
||||
// If the darkness level is outside of the darkness activation range, the light is disabled
|
||||
const darkness = canvas.darknessLevel;
|
||||
return !darkness.between(config.darkness.min, config.darkness.max);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does this Ambient Light actively emit darkness light given
|
||||
* its properties and the current darkness level of the Scene?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get emitsDarkness() {
|
||||
return this.document.config.negative && !this._isLightSourceDisabled();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does this Ambient Light actively emit positive light given
|
||||
* its properties and the current darkness level of the Scene?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get emitsLight() {
|
||||
return !this.document.config.negative && !this._isLightSourceDisabled();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_destroy(options) {
|
||||
this.#destroyLightSource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _draw(options) {
|
||||
this.field = this.addChild(new PIXI.Graphics());
|
||||
this.field.eventMode = "none";
|
||||
this.controlIcon = this.addChild(this.#drawControlIcon());
|
||||
this.initializeLightSource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw the ControlIcon for the AmbientLight
|
||||
* @returns {ControlIcon}
|
||||
*/
|
||||
#drawControlIcon() {
|
||||
const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);
|
||||
let icon = new ControlIcon({texture: CONFIG.controlIcons.light, size: size });
|
||||
icon.x -= (size * 0.5);
|
||||
icon.y -= (size * 0.5);
|
||||
return icon;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Incremental Refresh */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_applyRenderFlags(flags) {
|
||||
if ( flags.refreshState ) this._refreshState();
|
||||
if ( flags.refreshPosition ) this._refreshPosition();
|
||||
if ( flags.refreshField ) this._refreshField();
|
||||
if ( flags.refreshElevation ) this._refreshElevation();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the shape of the light field-of-effect. This is refreshed when the AmbientLight fov polygon changes.
|
||||
* @protected
|
||||
*/
|
||||
_refreshField() {
|
||||
this.field.clear();
|
||||
if ( !this.lightSource?.shape ) return;
|
||||
this.field.lineStyle(2, 0xEEEEEE, 0.4).drawShape(this.lightSource.shape);
|
||||
this.field.position.set(-this.lightSource.x, -this.lightSource.y);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the position of the AmbientLight. Called with the coordinates change.
|
||||
* @protected
|
||||
*/
|
||||
_refreshPosition() {
|
||||
const {x, y} = this.document;
|
||||
if ( (this.position.x !== x) || (this.position.y !== y) ) MouseInteractionManager.emulateMoveEvent();
|
||||
this.position.set(x, y);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the elevation of the control icon.
|
||||
* @protected
|
||||
*/
|
||||
_refreshElevation() {
|
||||
this.controlIcon.elevation = this.document.elevation;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the state of the light. Called when the disabled state or darkness conditions change.
|
||||
* @protected
|
||||
*/
|
||||
_refreshState() {
|
||||
this.alpha = this._getTargetAlpha();
|
||||
this.zIndex = this.hover ? 1 : 0;
|
||||
this.refreshControl();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the display of the ControlIcon for this AmbientLight source.
|
||||
*/
|
||||
refreshControl() {
|
||||
const isHidden = this.id && this.document.hidden;
|
||||
this.controlIcon.texture = getTexture(this.isVisible ? CONFIG.controlIcons.light : CONFIG.controlIcons.lightOff);
|
||||
this.controlIcon.tintColor = isHidden ? 0xFF3300 : 0xFFFFFF;
|
||||
this.controlIcon.borderColor = isHidden ? 0xFF3300 : 0xFF5500;
|
||||
this.controlIcon.elevation = this.document.elevation;
|
||||
this.controlIcon.refresh({visible: this.layer.active, borderVisible: this.hover || this.layer.highlightObjects});
|
||||
this.controlIcon.draw();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Light Source Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the LightSource associated with this AmbientLight object.
|
||||
* @param {object} [options={}] Options which modify how the source is updated
|
||||
* @param {boolean} [options.deleted=false] Indicate that this light source has been deleted
|
||||
*/
|
||||
initializeLightSource({deleted=false}={}) {
|
||||
const sourceId = this.sourceId;
|
||||
const wasLight = canvas.effects.lightSources.has(sourceId);
|
||||
const wasDarkness = canvas.effects.darknessSources.has(sourceId);
|
||||
const isDarkness = this.document.config.negative;
|
||||
const perceptionFlags = {
|
||||
refreshEdges: wasDarkness || isDarkness,
|
||||
initializeVision: wasDarkness || isDarkness,
|
||||
initializeLighting: wasDarkness || isDarkness,
|
||||
refreshLighting: true,
|
||||
refreshVision: true
|
||||
};
|
||||
|
||||
// Remove the light source from the active collection
|
||||
if ( deleted ) {
|
||||
if ( !this.lightSource?.active ) return;
|
||||
this.#destroyLightSource();
|
||||
canvas.perception.update(perceptionFlags);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-create source if it switches darkness state
|
||||
if ( (wasLight && isDarkness) || (wasDarkness && !isDarkness) ) this.#destroyLightSource();
|
||||
|
||||
// Create the light source if necessary
|
||||
this.lightSource ??= this.#createLightSource();
|
||||
|
||||
// Re-initialize source data and add to the active collection
|
||||
this.lightSource.initialize(this._getLightSourceData());
|
||||
this.lightSource.add();
|
||||
|
||||
// Assign perception and render flags
|
||||
canvas.perception.update(perceptionFlags);
|
||||
if ( this.layer.active ) this.renderFlags.set({refreshField: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the light source data.
|
||||
* @returns {LightSourceData}
|
||||
* @protected
|
||||
*/
|
||||
_getLightSourceData() {
|
||||
const {x, y, elevation, rotation, walls, vision} = this.document;
|
||||
const d = canvas.dimensions;
|
||||
return foundry.utils.mergeObject(this.config.toObject(false), {
|
||||
x, y, elevation, rotation, walls, vision,
|
||||
dim: Math.clamp(this.dimRadius, 0, d.maxR),
|
||||
bright: Math.clamp(this.brightRadius, 0, d.maxR),
|
||||
seed: this.document.getFlag("core", "animationSeed"),
|
||||
disabled: this._isLightSourceDisabled(),
|
||||
preview: this.isPreview
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Returns a new point source: DarknessSource or LightSource, depending on the config data.
|
||||
* @returns {foundry.canvas.sources.PointLightSource|foundry.canvas.sources.PointDarknessSource} The created source
|
||||
*/
|
||||
#createLightSource() {
|
||||
const sourceClass = this.config.negative ? CONFIG.Canvas.darknessSourceClass : CONFIG.Canvas.lightSourceClass;
|
||||
const sourceId = this.sourceId;
|
||||
return new sourceClass({sourceId, object: this});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Destroy the existing BaseEffectSource instance for this AmbientLight.
|
||||
*/
|
||||
#destroyLightSource() {
|
||||
this.lightSource?.destroy();
|
||||
this.lightSource = undefined;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Document Event Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onCreate(data, options, userId) {
|
||||
super._onCreate(data, options, userId);
|
||||
this.initializeLightSource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onUpdate(changed, options, userId) {
|
||||
super._onUpdate(changed, options, userId);
|
||||
this.initializeLightSource();
|
||||
this.renderFlags.set({
|
||||
refreshState: ("hidden" in changed) || (("config" in changed)
|
||||
&& ["dim", "bright", "angle", "darkness"].some(k => k in changed.config)),
|
||||
refreshElevation: "elevation" in changed
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onDelete(options, userId) {
|
||||
this.initializeLightSource({deleted: true});
|
||||
super._onDelete(options, userId);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Interactivity */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_canHUD(user, event) {
|
||||
return user.isGM; // Allow GMs to single right-click
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_canConfigure(user, event) {
|
||||
return false; // Double-right does nothing
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_canDragLeftStart(user, event) {
|
||||
// Prevent dragging another light if currently previewing one.
|
||||
if ( this.layer?.preview?.children.length ) {
|
||||
ui.notifications.warn("CONTROLS.ObjectConfigured", { localize: true });
|
||||
return false;
|
||||
}
|
||||
return super._canDragLeftStart(user, event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_onClickRight(event) {
|
||||
this.document.update({hidden: !this.document.hidden});
|
||||
if ( !this._propagateRightClick(event) ) event.stopPropagation();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_onDragLeftMove(event) {
|
||||
super._onDragLeftMove(event);
|
||||
this.initializeLightSource({deleted: true});
|
||||
const clones = event.interactionData.clones || [];
|
||||
for ( const c of clones ) c.initializeLightSource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_onDragEnd() {
|
||||
this.initializeLightSource({deleted: true});
|
||||
this._original?.initializeLightSource();
|
||||
super._onDragEnd();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
updateSource({deleted=false}={}) {
|
||||
const msg = "AmbientLight#updateSource has been deprecated in favor of AmbientLight#initializeLightSource";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||||
this.initializeLightSource({deleted});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get source() {
|
||||
const msg = "AmbientLight#source has been deprecated in favor of AmbientLight#lightSource";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||||
return this.lightSource;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user