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,3 @@
export {default as TokenRing} from "./ring.mjs";
export {default as TokenRingConfig} from "./ring-config.mjs";
export {default as DynamicRingData} from "./ring-data.mjs"

View File

@@ -0,0 +1,379 @@
import DynamicRingData from "./ring-data.mjs";
/**
* The start and end radii of the token ring color band.
* @typedef {Object} RingColorBand
* @property {number} startRadius The starting normalized radius of the token ring color band.
* @property {number} endRadius The ending normalized radius of the token ring color band.
*/
/**
* Dynamic ring id.
* @typedef {string} DynamicRingId
*/
/**
* Token Ring configuration Singleton Class.
*
* @example Add a new custom ring configuration. Allow only ring pulse, ring gradient and background wave effects.
* const customConfig = new foundry.canvas.tokens.DynamicRingData({
* id: "myCustomRingId",
* label: "Custom Ring",
* effects: {
* RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
* RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
* BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE"
* },
* spritesheet: "canvas/tokens/myCustomRings.json",
* framework: {
* shaderClass: MyCustomTokenRingSamplerShader,
* ringClass: TokenRing
* }
* });
* CONFIG.Token.ring.addConfig(customConfig.id, customConfig);
*
* @example Get a specific ring configuration
* const config = CONFIG.Token.ring.getConfig("myCustomRingId");
* console.log(config.spritesheet); // Output: canvas/tokens/myCustomRings.json
*
* @example Use a specific ring configuration
* const success = CONFIG.Token.ring.useConfig("myCustomRingId");
* console.log(success); // Output: true
*
* @example Get the labels of all configurations
* const configLabels = CONFIG.Token.ring.configLabels;
* console.log(configLabels);
* // Output:
* // {
* // "coreSteel": "Foundry VTT Steel Ring",
* // "coreBronze": "Foundry VTT Bronze Ring",
* // "myCustomRingId" : "My Super Power Ring"
* // }
*
* @example Get the IDs of all configurations
* const configIDs = CONFIG.Token.ring.configIDs;
* console.log(configIDs); // Output: ["coreSteel", "coreBronze", "myCustomRingId"]
*
* @example Create a hook to add a custom token ring configuration. This ring configuration will appear in the settings.
* Hooks.on("initializeDynamicTokenRingConfig", ringConfig => {
* const mySuperPowerRings = new foundry.canvas.tokens.DynamicRingData({
* id: "myCustomRingId",
* label: "My Super Power Rings",
* effects: {
* RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
* RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
* BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE"
* },
* spritesheet: "canvas/tokens/mySuperPowerRings.json"
* });
* ringConfig.addConfig("mySuperPowerRings", mySuperPowerRings);
* });
*
* @example Activate color bands debugging visuals to ease configuration
* CONFIG.Token.ring.debugColorBands = true;
*/
export default class TokenRingConfig {
constructor() {
if ( TokenRingConfig.#instance ) {
throw new Error("An instance of TokenRingConfig has already been created. " +
"Use `CONFIG.Token.ring` to access it.");
}
TokenRingConfig.#instance = this;
}
/**
* The token ring config instance.
* @type {TokenRingConfig}
*/
static #instance;
/**
* To know if the ring config is initialized.
* @type {boolean}
*/
static #initialized = false;
/**
* To know if a Token Ring registration is possible.
* @type {boolean}
*/
static #closedRegistration = true;
/**
* Core token rings used in Foundry VTT.
* Each key is a string identifier for a ring, and the value is an object containing the ring's data.
* This object is frozen to prevent any modifications.
* @type {Readonly<Record<DynamicRingId, RingData>>}
*/
static CORE_TOKEN_RINGS = Object.freeze({
coreSteel: {
id: "coreSteel",
label: "TOKEN.RING.SETTINGS.coreSteel",
spritesheet: "canvas/tokens/rings-steel.json"
},
coreBronze: {
id: "coreBronze",
label: "TOKEN.RING.SETTINGS.coreBronze",
spritesheet: "canvas/tokens/rings-bronze.json"
}
});
/**
* Core token rings fit modes used in Foundry VTT.
* @type {Readonly<object>}
*/
static CORE_TOKEN_RINGS_FIT_MODES = Object.freeze({
subject: {
id: "subject",
label: "TOKEN.RING.SETTINGS.FIT_MODES.subject"
},
grid: {
id: "grid",
label: "TOKEN.RING.SETTINGS.FIT_MODES.grid"
}
});
/* -------------------------------------------- */
/**
* Register the token ring config and initialize it
*/
static initialize() {
// If token config is initialized
if ( this.#initialized ) {
throw new Error("The token configuration class can be initialized only once!")
}
// Open the registration window for the token rings
this.#closedRegistration = false;
// Add default rings
for ( const id in this.CORE_TOKEN_RINGS ) {
const config = new DynamicRingData(this.CORE_TOKEN_RINGS[id]);
CONFIG.Token.ring.addConfig(config.id, config);
}
// Call an explicit hook for token ring configuration
Hooks.callAll("initializeDynamicTokenRingConfig", CONFIG.Token.ring);
// Initialize token rings configuration
if ( !CONFIG.Token.ring.useConfig(game.settings.get("core", "dynamicTokenRing")) ) {
CONFIG.Token.ring.useConfig(this.CORE_TOKEN_RINGS.coreSteel.id);
}
// Close the registration window for the token rings
this.#closedRegistration = true;
this.#initialized = true;
}
/* -------------------------------------------- */
/**
* Register game settings used by the Token Ring
*/
static registerSettings() {
game.settings.register("core", "dynamicTokenRing", {
name: "TOKEN.RING.SETTINGS.label",
hint: "TOKEN.RING.SETTINGS.hint",
scope: "world",
config: true,
type: new foundry.data.fields.StringField({required: true, blank: false,
initial: this.CORE_TOKEN_RINGS.coreSteel.id,
choices: () => CONFIG.Token.ring.configLabels
}),
requiresReload: true
});
game.settings.register("core", "dynamicTokenRingFitMode", {
name: "TOKEN.RING.SETTINGS.FIT_MODES.label",
hint: "TOKEN.RING.SETTINGS.FIT_MODES.hint",
scope: "world",
config: true,
type: new foundry.data.fields.StringField({
required: true,
blank: false,
initial: this.CORE_TOKEN_RINGS_FIT_MODES.subject.id,
choices: Object.fromEntries(Object.entries(this.CORE_TOKEN_RINGS_FIT_MODES).map(([key, mode]) => [key, mode.label]))
}),
requiresReload: true
});
}
/* -------------------------------------------- */
/**
* Ring configurations.
* @type {Map<string, DynamicRingData>}
*/
#configs = new Map();
/**
* The current ring configuration.
* @type {DynamicRingData}
*/
#currentConfig;
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* A mapping of token subject paths where modules or systems have configured subject images.
* @type {Record<string, string>}
*/
subjectPaths = {};
/**
* All color bands visual debug flag.
* @type {boolean}
*/
debugColorBands = false;
/**
* Get the current ring class.
* @type {typeof TokenRing} The current ring class.
*/
get ringClass() {
return this.#currentConfig.framework.ringClass;
}
set ringClass(value) {
this.#currentConfig.framework.ringClass = value;
}
/**
* Get the current effects.
* @type {Record<string, string>} The current effects.
*/
get effects() {
return this.#currentConfig.effects;
}
/**
* Get the current spritesheet.
* @type {string} The current spritesheet path.
*/
get spritesheet() {
return this.#currentConfig.spritesheet;
}
/**
* Get the current shader class.
* @type {typeof PrimaryBaseSamplerShader} The current shader class.
*/
get shaderClass() {
return this.#currentConfig.framework.shaderClass;
}
set shaderClass(value) {
this.#currentConfig.framework.shaderClass = value;
}
/**
* Get the current localized label.
* @returns {string}
*/
get label() {
return this.#currentConfig.label;
}
/**
* Get the current id.
* @returns {string}
*/
get id() {
return this.#currentConfig.id;
}
/* -------------------------------------------- */
/* Management */
/* -------------------------------------------- */
/**
* Is a custom fit mode active?
* @returns {boolean}
*/
get isGridFitMode() {
return game.settings.get("core","dynamicTokenRingFitMode")
=== this.constructor.CORE_TOKEN_RINGS_FIT_MODES.grid.id;
}
/* -------------------------------------------- */
/**
* Add a new ring configuration.
* @param {string} id The id of the ring configuration.
* @param {RingConfig} config The configuration object for the ring.
*/
addConfig(id, config) {
if ( this.constructor.#closedRegistration ) {
throw new Error("Dynamic Rings registration window is closed. You must register a dynamic token ring configuration during" +
" the `registerDynamicTokenRing` hook.");
}
this.#configs.set(id, config);
}
/* -------------------------------------------- */
/**
* Get a ring configuration.
* @param {string} id The id of the ring configuration.
* @returns {RingConfig} The ring configuration object.
*/
getConfig(id) {
return this.#configs.get(id);
}
/* -------------------------------------------- */
/**
* Use a ring configuration.
* @param {string} id The id of the ring configuration to use.
* @returns {boolean} True if the configuration was successfully set, false otherwise.
*/
useConfig(id) {
if ( this.#configs.has(id) ) {
this.#currentConfig = this.#configs.get(id);
return true;
}
return false;
}
/* -------------------------------------------- */
/**
* Get the IDs of all configurations.
* @returns {string[]} The names of all configurations.
*/
get configIDs() {
return Array.from(this.#configs.keys());
}
/* -------------------------------------------- */
/**
* Get the labels of all configurations.
* @returns {Record<string, string>} An object with configuration names as keys and localized labels as values.
*/
get configLabels() {
const labels = {};
for ( const [name, config] of this.#configs.entries() ) {
labels[name] = config.label;
}
return labels;
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
get configNames() {
const msg = "TokenRingConfig#configNames is deprecated and replaced by TokenRingConfig#configIDs";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
return this.configIDs;
}
}

View File

@@ -0,0 +1,81 @@
import TokenRing from "./ring.mjs";
import DataModel from "../../../common/abstract/data.mjs";
import {DataField} from "../../../common/data/fields.mjs";
/**
* @typedef {Object} RingData
* @property {string} id The id of this Token Ring configuration.
* @property {string} label The label of this Token Ring configuration.
* @property {string} spritesheet The spritesheet path which provides token ring frames for various sized creatures.
* @property {Record<string, string>} [effects] Registered special effects which can be applied to a token ring.
* @property {Object} framework
* @property {typeof TokenRing} [framework.ringClass=TokenRing] The manager class responsible for rendering token rings.
* @property {typeof PrimaryBaseSamplerShader} [framework.shaderClass=TokenRingSamplerShader] The shader class used to render the TokenRing.
*/
/**
* A special subclass of DataField used to reference a class definition.
*/
class ClassReferenceField extends DataField {
constructor(options) {
super(options);
this.#baseClass = options.baseClass;
}
/**
* The base class linked to this data field.
* @type {typeof Function}
*/
#baseClass;
/** @inheritdoc */
static get _defaults() {
const defaults = super._defaults;
defaults.required = true;
return defaults;
}
/** @override */
_cast(value) {
if ( !foundry.utils.isSubclass(value, this.#baseClass) ) {
throw new Error(`The value provided to a ClassReferenceField must be a ${this.#baseClass.name} subclass.`);
}
return value;
}
/** @override */
getInitialValue(data) {
return this.initial;
}
}
/* -------------------------------------------- */
/**
* Dynamic Ring configuration data model.
* @extends {foundry.abstract.DataModel}
* @implements {RingData}
*/
export default class DynamicRingData extends DataModel {
/** @inheritDoc */
static defineSchema() {
const fields = foundry.data.fields;
// Return model schema
return {
id: new fields.StringField({blank: true}),
label: new fields.StringField({blank: false}),
spritesheet: new fields.FilePathField({categories: ["TEXT"], required: true}),
effects: new fields.ObjectField({initial: {
RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
BKG_WAVE: "TOKEN.RING.EFFECTS.BKG_WAVE",
INVISIBILITY: "TOKEN.RING.EFFECTS.INVISIBILITY"
}}),
framework: new fields.SchemaField({
ringClass: new ClassReferenceField({initial: TokenRing, baseClass: TokenRing}),
shaderClass: new ClassReferenceField({initial: TokenRingSamplerShader, baseClass: PrimaryBaseSamplerShader})
})
};
}
}

View File

@@ -0,0 +1,508 @@
/**
* Dynamic Token Ring Manager.
*/
export default class TokenRing {
/**
* A TokenRing is constructed by providing a reference to a Token object.
* @param {Token} token
*/
constructor(token) {
this.#token = new WeakRef(token);
}
/* -------------------------------------------- */
/* Rings System */
/* -------------------------------------------- */
/**
* The start and end radii of the token ring color band.
* @typedef {Object} RingColorBand
* @property {number} startRadius The starting normalized radius of the token ring color band.
* @property {number} endRadius The ending normalized radius of the token ring color band.
*/
/* -------------------------------------------- */
/**
* The effects which could be applied to a token ring (using bitwise operations).
* @type {Readonly<Record<string, number>>}
*/
static effects = Object.freeze({
DISABLED: 0x00,
ENABLED: 0x01,
RING_PULSE: 0x02,
RING_GRADIENT: 0x04,
BKG_WAVE: 0x08,
INVISIBILITY: 0x10 // or spectral pulse effect
});
/* -------------------------------------------- */
/**
* Is the token rings framework enabled? Will be `null` if the system hasn't initialized yet.
* @type {boolean|null}
*/
static get initialized() {
return this.#initialized;
}
static #initialized = null;
/* -------------------------------------------- */
/**
* Token Rings sprite sheet base texture.
* @type {PIXI.BaseTexture}
*/
static baseTexture;
/**
* Rings and background textures UVs and center offset.
* @type {Record<string, {UVs: Float32Array, center: {x: number, y: number}}>}
*/
static texturesData;
/**
* The token ring shader class definition.
* @type {typeof TokenRingSamplerShader}
*/
static tokenRingSamplerShader;
/**
* Ring data with their ring name, background name and their grid dimension target.
* @type {{ringName: string, bkgName: string, colorBand: RingColorBand, gridTarget: number,
* defaultRingColorLittleEndian: number|null, defaultBackgroundColorLittleEndian: number|null,
* subjectScaleAdjustment: number}[]}
*/
static #ringData;
/**
* Default ring thickness in normalized space.
* @type {number}
*/
static #defaultRingThickness = 0.1269848;
/**
* Default ring subject thickness in normalized space.
* @type {number}
*/
static #defaultSubjectThickness = 0.6666666;
/* -------------------------------------------- */
/**
* Initialize the Token Rings system, registering the batch plugin and patching PrimaryCanvasGroup#addToken.
*/
static initialize() {
if ( TokenRing.#initialized ) return;
TokenRing.#initialized = true;
// Register batch plugin
this.tokenRingSamplerShader = CONFIG.Token.ring.shaderClass;
this.tokenRingSamplerShader.registerPlugin();
}
/* -------------------------------------------- */
/**
* Create texture UVs for each asset into the token rings sprite sheet.
*/
static createAssetsUVs() {
const spritesheet = TextureLoader.loader.getCache(CONFIG.Token.ring.spritesheet);
if ( !spritesheet ) throw new Error("TokenRing UV generation failed because no spritesheet was loaded!");
this.baseTexture = spritesheet.baseTexture;
this.texturesData = {};
this.#ringData = [];
const {
defaultColorBand={startRadius: 0.59, endRadius: 0.7225},
defaultRingColor: drc,
defaultBackgroundColor: dbc
} = spritesheet.data.config ?? {};
const defaultRingColor = Color.from(drc);
const defaultBackgroundColor = Color.from(dbc);
const validDefaultRingColor = defaultRingColor.valid ? defaultRingColor.littleEndian : null;
const validDefaultBackgroundColor = defaultBackgroundColor.valid ? defaultBackgroundColor.littleEndian : null;
const frames = Object.keys(spritesheet.data.frames || {});
for ( const asset of frames ) {
const assetTexture = PIXI.Assets.cache.get(asset);
if ( !assetTexture ) continue;
// Extracting texture UVs
const frame = assetTexture.frame;
const textureUvs = new PIXI.TextureUvs();
textureUvs.set(frame, assetTexture.baseTexture, assetTexture.rotate);
this.texturesData[asset] = {
UVs: textureUvs.uvsFloat32,
center: {
x: frame.center.x / assetTexture.baseTexture.width,
y: frame.center.y / assetTexture.baseTexture.height
}
};
// Skip background assets
if ( asset.includes("-bkg") ) continue;
// Extracting and determining final colors
const { ringColor: rc, backgroundColor: bc, colorBand, gridTarget, ringThickness=this.#defaultRingThickness }
= spritesheet.data.frames[asset] || {};
const ringColor = Color.from(rc);
const backgroundColor = Color.from(bc);
const finalRingColor = ringColor.valid ? ringColor.littleEndian : validDefaultRingColor;
const finalBackgroundColor = backgroundColor.valid ? backgroundColor.littleEndian : validDefaultBackgroundColor;
const subjectScaleAdjustment = 1 / (ringThickness + this.#defaultSubjectThickness);
this.#ringData.push({
ringName: asset,
bkgName: `${asset}-bkg`,
colorBand: foundry.utils.deepClone(colorBand ?? defaultColorBand),
gridTarget: gridTarget ?? 1,
defaultRingColorLittleEndian: finalRingColor,
defaultBackgroundColorLittleEndian: finalBackgroundColor,
subjectScaleAdjustment
});
}
// Sorting the rings data array
this.#ringData.sort((a, b) => a.gridTarget - b.gridTarget);
}
/* -------------------------------------------- */
/**
* Get the UVs array for a given texture name and scale correction.
* @param {string} name Name of the texture we want to get UVs.
* @param {number} [scaleCorrection=1] The scale correction applied to UVs.
* @returns {Float32Array}
*/
static getTextureUVs(name, scaleCorrection=1) {
if ( scaleCorrection === 1 ) return this.texturesData[name].UVs;
const tUVs = this.texturesData[name].UVs;
const c = this.texturesData[name].center;
const UVs = new Float32Array(8);
for ( let i=0; i<8; i+=2 ) {
UVs[i] = ((tUVs[i] - c.x) * scaleCorrection) + c.x;
UVs[i+1] = ((tUVs[i+1] - c.y) * scaleCorrection) + c.y;
}
return UVs;
}
/* -------------------------------------------- */
/**
* Get ring and background names for a given size.
* @param {number} size The size to match (grid size dimension)
* @returns {{bkgName: string, ringName: string, colorBand: RingColorBand}}
*/
static getRingDataBySize(size) {
if ( !Number.isFinite(size) || !this.#ringData.length ) {
return {
ringName: undefined,
bkgName: undefined,
colorBand: undefined,
defaultRingColorLittleEndian: null,
defaultBackgroundColorLittleEndian: null,
subjectScaleAdjustment: null
};
}
const rings = this.#ringData.map(r => [Math.abs(r.gridTarget - size), r]);
// Sort rings on proximity to target size
rings.sort((a, b) => a[0] - b[0]);
// Choose the closest ring, access the second element of the first array which is the ring data object
const closestRing = rings[0][1];
return {
ringName: closestRing.ringName,
bkgName: closestRing.bkgName,
colorBand: closestRing.colorBand,
defaultRingColorLittleEndian: closestRing.defaultRingColorLittleEndian,
defaultBackgroundColorLittleEndian: closestRing.defaultBackgroundColorLittleEndian,
subjectScaleAdjustment: closestRing.subjectScaleAdjustment
};
}
/* -------------------------------------------- */
/* Attributes */
/* -------------------------------------------- */
/** @type {string} */
ringName;
/** @type {string} */
bkgName;
/** @type {Float32Array} */
ringUVs;
/** @type {Float32Array} */
bkgUVs;
/** @type {number} */
ringColorLittleEndian = 0xFFFFFF; // Little endian format => BBGGRR
/** @type {number} */
bkgColorLittleEndian = 0xFFFFFF; // Little endian format => BBGGRR
/** @type {number|null} */
defaultRingColorLittleEndian = null;
/** @type {number|null} */
defaultBackgroundColorLittleEndian = null;
/** @type {number} */
effects = 0;
/** @type {number} */
scaleCorrection = 1;
/** @type {number} */
scaleAdjustmentX = 1;
/** @type {number} */
scaleAdjustmentY = 1;
/** @type {number} */
subjectScaleAdjustment = 1;
/** @type {number} */
textureScaleAdjustment = 1;
/** @type {RingColorBand} */
colorBand;
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* Reference to the token that should be animated.
* @type {Token|void}
*/
get token() {
return this.#token.deref();
}
/**
* Weak reference to the token being animated.
* @type {WeakRef<Token>}
*/
#token;
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/**
* Configure the sprite mesh.
* @param {PrimarySpriteMesh} [mesh] The mesh to which TokenRing functionality is configured.
*/
configure(mesh) {
this.#configureTexture(mesh);
this.configureSize();
this.configureVisuals();
}
/* -------------------------------------------- */
/**
* Clear configuration pertaining to token ring from the mesh.
*/
clear() {
this.ringName = undefined;
this.bkgName = undefined;
this.ringUVs = undefined;
this.bkgUVs = undefined;
this.colorBand = undefined;
this.ringColorLittleEndian = 0xFFFFFF;
this.bkgColorLittleEndian = 0xFFFFFF;
this.defaultRingColorLittleEndian = null;
this.defaultBackgroundColorLittleEndian = null;
this.scaleCorrection = 1;
this.scaleAdjustmentX = 1;
this.scaleAdjustmentY = 1;
this.subjectScaleAdjustment = 1;
this.textureScaleAdjustment = 1;
const mesh = this.token.mesh;
if ( mesh ) mesh.padding = 0;
}
/* -------------------------------------------- */
/**
* Configure token ring size.
*/
configureSize() {
const mesh = this.token.mesh;
// Ring size
const size = Math.min(this.token.document.width ?? 1, this.token.document.height ?? 1);
Object.assign(this, this.constructor.getRingDataBySize(size));
// Subject scale
const scale = this.token.document.ring.subject.scale ?? this.scaleCorrection ?? 1;
this.scaleCorrection = scale;
this.ringUVs = this.constructor.getTextureUVs(this.ringName, scale);
this.bkgUVs = this.constructor.getTextureUVs(this.bkgName, scale);
// Determine the longer and shorter sides of the image
const {width: w, height: h} = this.token.mesh.texture ?? this.token.texture;
let longSide = Math.max(w, h);
let shortSide = Math.min(w, h);
// Calculate the necessary padding
let padding = (longSide - shortSide) / 2;
// Determine padding for x and y sides
let paddingX = (w < h) ? padding : 0;
let paddingY = (w > h) ? padding : 0;
// Apply mesh padding
mesh.paddingX = paddingX;
mesh.paddingY = paddingY;
// Apply adjustments
const adjustment = shortSide / longSide;
this.scaleAdjustmentX = paddingX ? adjustment : 1.0;
this.scaleAdjustmentY = paddingY ? adjustment : 1.0;
// Apply texture scale adjustment for token without a subject texture and in grid fit mode
const inferred = (this.token.document.texture.src !== this.token.document._inferRingSubjectTexture());
if ( CONFIG.Token.ring.isGridFitMode && !inferred && !this.token.document._source.ring.subject.texture ) {
this.textureScaleAdjustment = this.subjectScaleAdjustment;
}
else this.textureScaleAdjustment = 1;
}
/* -------------------------------------------- */
/**
* Configure the token ring visuals properties.
*/
configureVisuals() {
const ring = this.token.document.ring;
// Configure colors
const colors = foundry.utils.mergeObject(ring.colors, this.token.getRingColors(), {inplace: false});
const resolveColor = (color, defaultColor) => {
const resolvedColor = Color.from(color ?? 0xFFFFFF).littleEndian;
return ((resolvedColor === 0xFFFFFF) && (defaultColor !== null)) ? defaultColor : resolvedColor;
};
this.ringColorLittleEndian = resolveColor(colors?.ring, this.defaultRingColorLittleEndian);
this.bkgColorLittleEndian = resolveColor(colors?.background, this.defaultBackgroundColorLittleEndian)
// Configure effects
const effectsToApply = this.token.getRingEffects();
this.effects = ((ring.effects >= this.constructor.effects.DISABLED)
? ring.effects : this.constructor.effects.ENABLED)
| effectsToApply.reduce((acc, e) => acc |= e, 0x0);
// Mask with enabled effects for the current token ring configuration
let mask = this.effects & CONFIG.Token.ring.ringClass.effects.ENABLED;
for ( const key in CONFIG.Token.ring.effects ) {
const v = CONFIG.Token.ring.ringClass.effects[key];
if ( v !== undefined ) {
mask |= v;
}
}
this.effects &= mask;
}
/* -------------------------------------------- */
/**
* Configure dynamic token ring subject texture.
* @param {PrimarySpriteMesh} mesh The mesh being configured
*/
#configureTexture(mesh) {
const src = this.token.document.ring.subject.texture;
if ( PIXI.Assets.cache.has(src) ) {
const subjectTexture = getTexture(src);
if ( subjectTexture?.valid ) mesh.texture = subjectTexture;
}
}
/* -------------------------------------------- */
/* Animations */
/* -------------------------------------------- */
/**
* Flash the ring briefly with a certain color.
* @param {Color} color Color to flash.
* @param {CanvasAnimationOptions} animationOptions Options to customize the animation.
* @returns {Promise<boolean|void>}
*/
async flashColor(color, animationOptions={}) {
if ( Number.isNaN(color) ) return;
const defaultColorFallback = this.token.ring.defaultRingColorLittleEndian ?? 0xFFFFFF;
const configuredColor = Color.from(foundry.utils.mergeObject(
this.token.document.ring.colors,
this.token.getRingColors(),
{inplace: false}
).ring);
const originalColor = configuredColor.valid ? configuredColor.littleEndian : defaultColorFallback;
return await CanvasAnimation.animate([{
attribute: "ringColorLittleEndian",
parent: this,
from: originalColor,
to: new Color(color.littleEndian),
color: true
}], foundry.utils.mergeObject({
duration: 1600,
priority: PIXI.UPDATE_PRIORITY.HIGH,
easing: this.constructor.createSpikeEasing(.15)
}, animationOptions));
}
/* -------------------------------------------- */
/**
* Create an easing function that spikes in the center. Ideal duration is around 1600ms.
* @param {number} [spikePct=0.5] Position on [0,1] where the spike occurs.
* @returns {Function(number): number}
*/
static createSpikeEasing(spikePct=0.5) {
const scaleStart = 1 / spikePct;
const scaleEnd = 1 / (1 - spikePct);
return pt => {
if ( pt < spikePct ) return CanvasAnimation.easeInCircle(pt * scaleStart);
else return 1 - CanvasAnimation.easeOutCircle(((pt - spikePct) * scaleEnd));
};
}
/* -------------------------------------------- */
/**
* Easing function that produces two peaks before returning to the original value. Ideal duration is around 500ms.
* @param {number} pt The proportional animation timing on [0,1].
* @returns {number} The eased animation progress on [0,1].
*/
static easeTwoPeaks(pt) {
return (Math.sin((4 * Math.PI * pt) - (Math.PI / 2)) + 1) / 2;
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* To avoid breaking dnd5e.
* @deprecated since v12
* @ignore
*/
configureMesh() {}
/**
* To avoid breaking dnd5e.
* @deprecated since v12
* @ignore
*/
configureNames() {}
}