If you want to support more modules of this kind, I invite you to go and support the original author to ko-fi , or with paypal
Token Magic is a module for Foundry VTT that allows you to add graphic effects to tokens, tiles, drawings and templates. These FX can be animated.
This beta version is partly used via macros, except for automatic templates and measurement templates. A graphical interface will be added later. But do not panic, Token Magic comes with a compendium of macros for each effect, easily modifiable to suit your needs. I advise you to go there and play with the values.
In short :
If you are a streamer and want your video about TMFX to be added to these lists, contact me on Discord or Github.
TokenMagic can apply the following effects: Bloom, distortion, old film effect, glow, outline, bevel, shadow drop, shadow zap, twist, blur, zoom blur, shockwave, bulge, pinch, inner rays, inner fog, color adjustment (transparency, contrast, brightness, color balance, etc.), and many more! All properties linked to these effects can be animated.
Token Magic is adding four options :
You can configure automatic templates in the module option panel. You also have options to disable automatic templates and the display of the grid.
In the template settings, you have a tab that display to automatic templates settings by categories, and another for spells and items overrides.
Automatic template settings are linked to a world. You can import and export the settings with the following code :
To export your template settings in a json file
TokenMagic.exportTemplateSettings(optional <exportName>);
To import template settings from a json file (open a file picker dialog). Warning : your settings are REPLACED ! Before, you may want to save your data with the code above.
(async) TokenMagic.importTemplateSettings();
On drawings, tiles, and Tokens. You must work with macros. You will find a lot of predefined macros in the compendium.
You can use them directly or create your own by modifying/improving them. Just think to create a copy of the Token Magic FX Compendium.
Here an example of a Token Magic macro.
let params =
[{
filterType: "glow",
filterId: "tokenmagic-example",
distance: 10,
outerStrength: 3,
color: 0x003000,
padding: 25,
animated:
{
color:
{
active: true,
loopDuration: 3000,
animType: "colorOscillation",
val1: 0x003000,
val2: 0x00FF00
}
}
},
{
filterType: "shadow",
rotation: 35,
blur: 2,
quality: 10,
distance: 20,
alpha: 0.7,
padding: 5,
color: 0x000000,
animated:
{
blur:
{
active: true,
loopDuration: 500,
animType: "syncCosOscillation",
val1: 2,
val2: 4
}
}
}];
TokenMagic.addFiltersOnSelected(params);
The filterType
allows you to specify the type of filter you want to apply: in this case, this is a Glow effect.
Each filter is identified by a filterId
, you can specify it. Otherwise, a random one will be automatically assigned by the module.
distance
, outerStrength
and color
are attributes specific to the filter (see the macro portfolio in the compendium)
Padding
is a property applicable to all filters. It allows you to increase the size of the container so that the effects do not spill over an invisible part. Padding is applied on all sides. In the module option, you can decide to use the default additive padding (three filters with a padding of 40 would make a padding of 120), use a maximum padding (one filter with a padding of 10 and a filter with a padding of 20 give a padding of 20). You can also assign a minimum padding value that will be assigned each time you create an effect.
You want to apply filters to all selected tokens or tiles in the UI ? Use :
TokenMagic.addFiltersOnSelected(<array of filters params>)
You want to apply the filters to a placeable passed in parameter (token, tile, drawing, template) ? Use :
TokenMagic.addFilters(<placeable>, <array of filters params>)
To remove filters :
TokenMagic.deleteFiltersOnSelected()
TokenMagic.deleteFilters(<placeable>)
Adding filters to targeted tokens
(async) TokenMagic.addFiltersOnTargeted(<array of filters params>)
Deleting all filters of targeted tokens
(async) TokenMagic.deleteFiltersOnTargeted()
The update params are exactly on the same model when you use them to create filters, but you put only the properties you want to change. However, you need to put thefilterId
. All filters matching the filterId will be updated. You can restrain the updates to targets, selections, or even by placeable :
(async) TokenMagic.updateFiltersOnSelected(<array of update params>)
(async) TokenMagic.updateFiltersOnTargeted(<array of update params>)
(async) TokenMagic.updateFiltersByPlaceable(<array of update params>, <placeable>)
Below an example with a glow filter :
let params =
[{
filterType: "glow",
filterId: "mySuperSpookyGlow",
distance: 10,
outerStrength: 8,
innerStrength: 0,
color: 0x003000,
quality: 0.5,
padding: 10,
animated:
{
color:
{
active: true,
loopDuration: 3000,
animType: "colorOscillation",
val1:0x003000,
val2:0x00FF00
}
}
}];
TokenMagic.addFiltersOnSelected(params);
I need to update the outerStrength
and the animated color properties, val1
and val2
of "mySuperSpookyGlow"
on all targeted tokens :
let params =
[{
filterId: "mySuperSpookyGlow",
outerStrength: 6,
animated:
{
color:
{
val1:0x300030,
val2:0xFF3000
}
}
}];
TokenMagic.updateFiltersOnTargeted(params);
Easy ! Later, I want to disable "mySuperSpookyGlow"
on all the selected tokens.
// disabling is not destroying. The filter is still here, but dormant. When disabled, a filter no longer consumes CPU or GPU cycles. (you can enable a disabled filter with enabled: true)
// Concerning the animations, you can put active:false to stop it or active:true to resume it. It's better to stop an animation by putting loops:1 (if loops is a possible animation property). In loops, you can put Infinity (loops: Infinity)
let params =
[{
filterId: "mySuperSpookyGlow",
enabled: false
}];
TokenMagic.updateFiltersOnSelected(params);
delete
functions support an optional parameter : filterId
. They will restrict deletion only on filters which match the filterId passed in parameter. Other filters will remain untouched.
// Example
TokenMagic.deleteFiltersOnTargeted("poisonSmoke_1");
TokenMagic.deleteFiltersOnSelected("mySuperSpookyGlow");
// Etc...
To avoid call to deleteFilters, you can specify that you want to replace (therefore delete) possible existing filters when calling the functions below :
TokenMagic.addFilters(<placeable>, <paramsArray>, optional <replace> = false);
TokenMagic.addFiltersOnTargeted(<paramsArray>, optional <replace> = false);
TokenMagic.addFiltersOnSelected(<paramsArray>, optional <replace> = false);
// Example:
let params =
[{
filterType: "glow",
filterId: "tokenmagic-example",
outerStrength: 3,
color: 0x003000,
padding: 20
},
{
filterType: "shadow",
filterId: "tokenmagic-example",
rotation: 35,
blur: 2,
quality: 10,
distance: 20,
alpha: 0.7,
color: 0x000000,
}];
// All possible existing filters will be deleted on the selected placeables before applying the new filters
// Technical precision -> it is just one call to a setFlag which add and delete the filters
TokenMagic.addFiltersOnSelected(params, true);
To verify if a placeable has a filter with the specified filterType
:
TokenMagic.hasFilterType(<placeable>,<filterType>)
// Example
...
if (TokenMagic.hasFilterType(myToken,"glow")) {
console.log("myToken has a glow filter.");
}
...
To verify if a placeable has a filter with the specified filterId
:
TokenMagic.hasFilterId(<placeable>,<filterId>)
// Example
...
if (TokenMagic.hasFilterId(myToken,"mySuperShadow_01")) {
console.log("myToken has my customized super shadow 1 filter.");
}
...
Here are one in two functions, to add or update filter(s) on a specific placeable, selected tokens, drawings or tiles, or targeted tokens. If a filter applied on an object has a filterType and a filterId identical to those found in the parameters, the values are updated with the new ones. Otherwise a new filter is created.
(async) TokenMagic.addUpdateFilters(<placeable>, <array of params>)
(async) TokenMagic.addUpdateFiltersOnSelected(<array of params>)
(async) TokenMagic.addUpdateFiltersOnTargeted(<array of params>)
You can automatically destroy or disable filters with the following properties :
autoDisable
: When this property is set to true
the filter is automatically disabled when each animation become inactive (number of loops reached).autoDestroy
: The same as autoDisable, but the filter is destroyed.loops
with Infinity
(default value if the property is not present), you will never trigger the autoDestroy or autoDisable. But you can prepare your filter with an auto keyword and then, later, update the loops
properties with finite values to start the countdown.// autoDestroy example
let params =
[{
filterType: "glow",
filterId: "mySuperSpookyGlow",
autoDestroy: true,
outerStrength: 4,
padding: 10,
animated:
{
color:
{
active: true,
loopDuration: 3000,
loops: 5,
animType: "colorOscillation",
val1:0x003000,
val2:0x00FF00
}
}
}];
TokenMagic.addFiltersOnSelected(params);
To start an animation, you need to use the animated
keyword property. It means that you want to start the animation of a filter property, it contains its own keywords. In the above example, it is the color
property that we want to animate with an animation of type "colorOscillation"
. The cycle duration is 3000 ms and the value limits are val1
and val2
.
animated :
{
<property to animate> :
{
active: <true|false(default:false)> // filter active/inactive
loopDuration: <duration of the loop in ms(default:3000)>,
loops: <number of loops before inactivity(default:Infinity)>,
animType: <"animation keyword">,
val1: <value limit 1>, // used for a lot of things (oscillation, random gen, etc.)
val2: <value limit 2>, // used for a lot of things (oscillation, random gen, etc.)
wantInteger: <true|false(default:false)> // used for random generation
speed: <value> // used with « move » animType
chaosFactor: <value between 0 and 1(default:0.25)> // used to create chaos
syncShift: <value between 0 and 1(default:0)> // used to alter synchronicity
}
<,<other properties to animate>…>
}
Keywords: "cosOscillation"
"sinOscillation"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: loops
syncShift
A smooth transition between two values, val1 and val2, with a return to val2 that take loopDuration milliseconds before repetition. Theses oscillations have their own synchronization. cosOscillation begin at val1, while sinOscillation begin at half-range.
Keywords: "syncCosOscillation"
"syncSinOscillation"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: loops
syncShift
Same as above, but the transitions are globally synchronized. A syncShift can be applied to alter the global synchronization for this effect (0.25 for half). Since the transitions are synchronized, the value of the property can begin anywhere between val1 and val2.
Keywords: "chaoticOscillation"
"syncChaoticOscillation"
Mandatory properties: active
loopDuration
val1
val2
chaosFactor
Optional properties: loops
syncShift
The same as cosOscillation, but with a chaos factor. The oscillation is freaky. chaosFactor go from 0 (no chaos, huh?) to 1 (total chaos, you call that an oscillation?). A syncChaoticOscillation also exists if youd dare to synchronize chaos.
Keywords: "colorOscillation"
"syncColorOscillation"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: loops
syncShift
Same as cosOscillation or syncCosOscillation, but specific to colors.
The transition is correctly applied to each component of the RGB value.
Instead, you can use a cosOscillation, but the transition is not very nice (for colors).
Keywords: "halfCosOscillation"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: loops
syncShift
A smooth transition between two values, val1 and val2. The loopDuration
brings to val2. Theses oscillations have their own synchronization.
Useful when you want to control an alternance between val1 and val2 by using loops
Keywords: "halfColorOscillation"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: loops
syncShift
A smooth transition between two colors, val1
and val2
. The loopDuration
brings to val2. Theses oscillations have their own synchronization.
Useful when you want to control an alternance between val1 and val2 by using loops
Keywords: "rotation"
"syncRotation"
Mandatory properties: active
loopDuration
Optional properties: loops
syncShift
clockwise
You need to rotate something? it's for you. A full turn in loopDuration. You can set a counter-clockwise rotation with clockwise: false
. You want to synchronize all your rotations? Use syncRotation. You need to rotate 45 ° and return to 0 ° and vice versa? Use a cosOscillation.
Keywords: "randomNumber"
Mandatory properties: active
val1
val2
Optional properties: wantInteger
generate a random number per frame, between val1 and val2. You need Integers? Use wantInteger: true
Keywords: "randomNumberPerLoop"
Mandatory properties: active
loopDuration
val1
val2
Optional properties: wantInteger
loops
generate a random number at the end of a complete loop, between val1 and val2. You need Integers? Use wantInteger: true
Keywords: "randomColorPerLoop"
Mandatory properties: active
loopDuration
Optional properties: loops
generate a random color per complete loop.
Keywords: "move"
Mandatory properties: active
speed
increment a property value by his speed, in pixel / ms.
To randomize filter properties you can use the randomized
keyword property:
// Randomization example
{
filterType: "sprite",
filterId: "randomOverlay",
repeat: true,
alphaDiscard: true,
inverse: true,
gridPadding: 1,
translationX: 0,
translationY: 0,
top: true,
randomized: {
active: true,
imagePath: [
"modules/tokenmagic/fx/assets/distortion-1.png",
"modules/tokenmagic/fx/assets/dots-1.png",
"modules/tokenmagic/fx/assets/extrusion-1.png",
"modules/tokenmagic/fx/assets/noise-2.jpg",
"modules/tokenmagic/fx/assets/symbols-1.png",
"modules/tokenmagic/fx/assets/waves-1.png",
],
rotation: { val1: 0, val2: 360, step: 1 },
scaleX: { val1: 0.1, val2: 0.6, step: 0.01, link: 'scaleY' },
},
}
In the above example, imagePath
, scaleX
, and rotation
values are randomized.
imagePath
will pick one value from the provided listrotation
will pick a random integer in the range of 0 and 360 as defined byval1
and val
scaleX
will pack a random float between 0.1 and 0.6 and will assign the same value to scaleY
randomized :
{
active: <true|false(default:false)>, // randomizer active/inactive
<property to randomize> :
{
val1: <value>, // value limit 1
val2: <value>, // value limit 2
step: <value greater than 0(default:1)> // granularity of randomized value
list: <a list of values> // list that can be used instead of a range defined by 'val1' and 'val2'
link: <"property name"> // property to be assigned the same randomized value
},
<property to randomize> : <a list of values> // list of values to be randomly picked from
<,<other properties to animate>…>
}
You can control the ordering of the filters with the zOrder
property.
If you want to work with this new property, you must activate the option in the module option panel. The zOrder
allows the filters to be applied on a Placeable in a specific order: from the smallest zOrder
to the highest.
A concrete example: you have applied an aura to a token, then you apply a mirror image, the aura will also be processed by the mirror image, which is probably not the desired effect.
By assigning a zOrder
to your effects, you can determine that the mirror image should apply before the aura.
You will find below a table with the filters and their default zOrder
. The default zOrder
can be overriden in the parameters of the filters.
Filter | default zOrder | Image |
---|---|---|
Splash (splash) | 5 | |
Remove Shadow (zapshadow) | 10 | |
Pixelate (pixel) | 20 | |
Adjustment (adjustment) | 30 | |
Bloom (xbloom) | 40 | |
Outline (outline) | 50 | |
Old Film (oldfilm) | 60 | |
Glow (glow) | 70 | |
Gleaming Glow (xglow) | 80 | |
Bevel (bevel) | 90 | |
Mirror Images (images) | 100 | |
Drop Shadow (shadow) | 110 | |
Rays (ray) | 120 | |
XRays (xray) | 130 | |
BulgePinch (bulgepinch) | 140 | |
Fire (fire) | 150 | |
Electric (electric) | 160 | |
Flood (flood) | 170 | |
Liquid (liquid) | 180 | |
Fog (fog) | 190 | |
Smoke (smoke) | 200 | |
Fumes (fumes) | 210 | |
Shockwave (shockwave) | 220 | |
XFog (xfog) | 230 | |
Twist (twist) | 240 | |
Solar Ripples (ripples) | 250 | |
Spiderweb (web) | 260 | |
Globes (globes) | 270 | |
Waves (wave) | 280 | |
Blur (blur) | 290 | |
Zoom Blur (zoomblur) | 300 | |
Ascii (ascii) | 310 | |
Dot Shade (dot) | 320 | |
CRT Monitor (crt) | 330 | |
RGB Split (rgbSplit) | 340 | |
Transform (transform) | 1000 | |
Force Field (field) | 2000 | |
Distortion (distortion) | 4000 |
You can store your custom effects in a Token Magic library. TMFX comes with two libraries: A main library with all the presets common to drawings, tokens and tiles, and one specific to measured templates.
To add a preset in your library :
(async) TokenMagic.addPreset(<presetName>,<params>,optional <silent>)
// Example
// You don't need to add a filterId when creating a preset. The filterId is created with the preset name.
// In the example below, the filterId will be equal to "FunnyGlow"
// silent to true if you don't want a confirmation message to appear when the preset is created
let params =
[{
filterType: "glow",
color: 0x4090FF
}];
TokenMagic.addPreset("MyCustomGlow",params);
To delete a preset in your main library :
(async) TokenMagic.deletePreset(<presetName>,optional <silent>);
To get a preset from the main library :
TokenMagic.getPreset(<presetName>);
To export your library in a json file
TokenMagic.exportPresetLibrary(optional <exportName>);
To import presets into your library (open a file picker dialog)
(async) TokenMagic.importPresetLibrary();
To import presets into your library from a local path
(async) TokenMagic.importPresetLibraryFromPath(<path>);
To import presets into your library from an URL
(async) TokenMagic.importPresetLibraryFromURL(<URL>);
To reset the preset library with the default presets (confirmation is requested)
(async) TokenMagic.resetPresetLibrary();
An option in the module settings allow overwrite of duplicates (by preset name) on import. By default, duplicates are ignored.
To retrieve all presets from a library (by default in the main library : "tmfx-main"). Returns an array.
TokenMagic.getPresets(optional <libraryName>)
// Example to get the main presets
let tmfxMainPresets = TokenMagic.getPresets();
// Equivalent to TokenMagic.getPresets("tmfx-main");
// Example to get the template presets
let tmfxTemplatePresets = TokenMagic.getPresets("tmfx-template");
You can pass an object as a parameter. The object must contain the name and optionnaly, the library and various properties. If you don't specify a library, getPreset will look for the main library by default.
TokenMagic.getPreset(<presetName>|<params object>);
// Example 1:
// Classic method
// Search in the main library
let myFx = TokenMagic.getPreset("myFX");
// Example 2:
// You want to get a "Wild Magic" preset in the template library
let pstParams =
{
name: "Wild Magic",
library: "tmfx-template"
};
let tmfxWildMagicPst = TokenMagic.getPreset(pstParams);
// Example 3:
// You can override properties in the presets
// All filters of the preset containing the properties will be changed
// Currently does not work on nested properties, will come in a later version
let pstParams =
{
name: "Wild Magic",
library: "tmfx-template",
color: 0x00FF00
};
let tmfxWildMagicPst = TokenMagic.getPreset(pstParams);
// all colors properties values returned by the preset have been changed to 0x00FF00
To add a preset, you can specify library in an object (same as getPreset). Also, when you create a preset for a template (template library), you can add a default texture (used if no texture is setted for a given template) the same when deleting a preset
TokenMagic.addPreset(<presetName>|<object>, <params>, optional <silent>);
TokenMagic.deletePreset(<presetName>|<object>, optional <silent>);
// Example 1:
// Classic method
// Add in the main library
let params =
[{
filterType: "glow",
color: 0x00FF00,
outerStrength: 5
}];
TokenMagic.addPreset("My Glow",params);
// Example 2:
// using the same params, but adding to the template library with a default texture
// in silent mode
let presetDef =
{
name: "My Glow",
library: "tmfx-template",
defaultTexture: "modules/tokenmagic/fx/assets/templates/black-tone-strong-opacity.png"
};
TokenMagic.addPreset(presetDef, params, true);
// Example 3:
// You can create your own library (for a module for example)
// I do not enforce anything, but you should add a suffix corresponding to your module (or others)
let presetDef =
{
name: "Glowing Death",
library: "au5e-conditions"
};
TokenMagic.addPreset(presetDef, params);
// Example 4:
// deleting a preset in the template library
TokenMagic.deletePreset({name:"Glowing Death",library:"tmfx-template"});
You can use presets with those functions below, by replacing the params with a preset name :
TokenMagic.addFilters
TokenMagic.addFiltersOnSelected
TokenMagic.addFiltersOnTargeted
TokenMagic.addUpdateFiltersOnSelected
TokenMagic.addUpdateFiltersOnTargeted
TokenMagic.updateFiltersOnSelected
TokenMagic.updateFiltersOnTargeted
TokenMagic.updateFiltersByPlaceable
// Example
TokenMagic.addFiltersOnTargeted("dead");
The Token Magic API allows templates creation.
When creating templates, you can pass parameters to add special fx, opacity and tint.
All these new parameters are optionals. If you do not pass a texture parameter, TMFX will apply the FX preset default texture (if present).
MeasuredTemplate.create({
t: "cone",
user: game.user._id,
x: canvas.stage.pivot.x,
y: canvas.stage.pivot.y,
direction: 180,
angle: 57,
distance: 15,
borderColor: "#FF0000",
fillColor: "#FF3366",
flags :
{
tokenmagic:
{
options:
{
tmfxPreset: "Wild Magic",
tmfxTint: 0x00FF90,
tmfxTextureAlpha: 0.50
}
}
}
});
You have access to TMFX specific prototype functions in class PlaceableObject (Token, Tile, etc.), to facilitate coding :
(async) <PlaceableObject>.TMFXaddFilters(<params array>)
(async) <PlaceableObject>.TMFXupdateFilters(<params array>)
(async) <PlaceableObject>.TMFXaddUpdateFilters(<params array>)
(async) <PlaceableObject>.TMFXdeleteFilters(optional <filterId>)
<PlaceableObject>.TMFXhasFilterType(<filterType>)
<PlaceableObject>.TMFXhasFilterId(<filterId>)
// Example 1
let glowFunc = async function() {
const tokens = canvas.tokens.placeables;
for (const token of tokens){
if (token.TMFXhasFilterId("funnyAlternateGlow")) {
await token.TMFXdeleteFilters("funnyAlternateGlow");
} else {
let params =
[{
filterType: "glow",
filterId: "funnyAlternateGlow",
color: Math.floor(Math.random() * 16777215),
animated: null
}];
await token.TMFXaddUpdateFilters(params);
}
}
};
glowFunc();
Any issues, bugs, or feature requests are always welcome to be reported directly to the Issue Tracker, or using the Bug Reporter Module.
If you want to use TokenMagic in your modules, know that you are welcome. I await your feedback and I will listen to your needs.
Contributors are welcome: UI specialists and GLSL shader programmers would be appreciated, and anyone wishing to get involved in this project. Contact me on Github or Discord, or make a Super Surprise PR!
TokenMagic is in beta version, this means that bugs and other unwanted effects will occur from time to time. You can help me to track them by posting issues in Github and taking care to give me all the information I need to reproduce them. Thank you !
The wise man knows how to await
.
For those who wish to make a donation : , my paypal
My most sincere thanks.
SecretFire
Discord : SecretFire#4843