All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1229 lines
39 KiB

  1. import { CORE_SHAPE, DEFAULT_OVERLAY_CONFIG, OVERLAY_SHAPES } from '../scripts/models.js';
  2. import { VALID_EXPRESSION, getAllEffectMappings } from '../scripts/hooks/effectMappingHooks.js';
  3. import { evaluateOverlayExpressions, genTexture } from '../scripts/token/overlay.js';
  4. import { SEARCH_TYPE } from '../scripts/utils.js';
  5. import { showArtSelect } from '../token-variants.mjs';
  6. import { sortMappingsToGroups } from './effectMappingForm.js';
  7. import { getFlagMappings } from '../scripts/settings.js';
  8. import { Reticle } from '../scripts/reticle.js';
  9. export class OverlayConfig extends FormApplication {
  10. constructor(config, callback, id, token) {
  11. super({}, {});
  12. this.config = config ?? {};
  13. this.config.id = id;
  14. this.callback = callback;
  15. this.token = canvas.tokens.get(token._id);
  16. this.previewConfig = deepClone(this.config);
  17. }
  18. static get defaultOptions() {
  19. return mergeObject(super.defaultOptions, {
  20. id: 'token-variants-overlay-config',
  21. classes: ['sheet'],
  22. template: 'modules/token-variants/templates/overlayConfig.html',
  23. resizable: false,
  24. minimizable: false,
  25. title: 'Overlay Settings',
  26. width: 500,
  27. height: 'auto',
  28. //tabs: [{ navSelector: '.sheet-tabs', contentSelector: '.content', initial: 'misc' }, ],
  29. tabs: [
  30. { navSelector: '.tabs[data-group="main"]', contentSelector: 'form', initial: 'misc' },
  31. { navSelector: '.tabs[data-group="html"]', contentSelector: '.tab[data-tab="html"]', initial: 'template' },
  32. ],
  33. });
  34. }
  35. /**
  36. * @param {JQuery} html
  37. */
  38. activateListeners(html) {
  39. super.activateListeners(html);
  40. html.find('.reticle').on('click', (event) => {
  41. const icons = this.getPreviewIcons();
  42. if (icons.length) {
  43. Reticle.activate({ tvaOverlay: icons[0].icon, app: this, config: this.previewConfig });
  44. }
  45. });
  46. const imgLinkDisable = function (disabled) {
  47. html.find('.img-link-disable').prop('disabled', disabled);
  48. };
  49. html.find('.image-link').on('click', (event) => {
  50. const chkBox = $(event.target).closest('.form-group').find('[name="imgLinked"]');
  51. const button = $(event.target).closest('button');
  52. if (chkBox.is(':checked')) {
  53. chkBox.prop('checked', false);
  54. button.removeClass('active');
  55. imgLinkDisable(false);
  56. } else {
  57. chkBox.prop('checked', true);
  58. button.addClass('active');
  59. imgLinkDisable(true);
  60. }
  61. });
  62. imgLinkDisable(Boolean(this.config.imgLinked));
  63. html.find('.repeat').on('change', (event) => {
  64. const fieldset = $(event.target).closest('fieldset');
  65. const content = fieldset.find('.content');
  66. if (event.target.checked) {
  67. content.show();
  68. fieldset.addClass('active');
  69. } else {
  70. content.hide();
  71. fieldset.removeClass('active');
  72. }
  73. this.setPosition();
  74. });
  75. // Insert Controls to the Shape Legend
  76. const shapeLegends = html.find('.shape-legend');
  77. let config = this.config;
  78. shapeLegends.each(function (i) {
  79. const legend = $(this);
  80. legend.append(
  81. `&nbsp;<a class="cloneShape" data-index="${i}" title="Clone"><i class="fas fa-clone"></i></a>
  82. &nbsp;<a class="deleteShape" data-index="${i}" title="Remove"><i class="fas fa-trash-alt"></i></a>`
  83. );
  84. if (i != 0) {
  85. legend.append(
  86. `&nbsp;<a class="moveShapeUp" data-index="${i}" title="Move Up"><i class="fas fa-arrow-up"></i></a>`
  87. );
  88. }
  89. if (i != shapeLegends.length - 1) {
  90. legend.append(
  91. `&nbsp;<a class="moveShapeDown" data-index="${i}" title="Move Down"><i class="fas fa-arrow-down"></i></a>`
  92. );
  93. }
  94. legend.append(
  95. `<input class="shape-legend-input" type="text" name="shapes.${i}.label" value="${
  96. config.shapes?.[i]?.label ?? ''
  97. }">`
  98. );
  99. });
  100. // Shape listeners
  101. html.find('.addShape').on('click', this._onAddShape.bind(this));
  102. html.find('.addEvent').on('click', this._onAddEvent.bind(this));
  103. html.find('.deleteShape').on('click', this._onDeleteShape.bind(this));
  104. html.find('.deleteEvent').on('click', this._onDeleteEvent.bind(this));
  105. html.find('.moveShapeUp').on('click', this._onMoveShapeUp.bind(this));
  106. html.find('.moveShapeDown').on('click', this._onMoveShapeDown.bind(this));
  107. html.find('.cloneShape').on('click', this._onCloneShape.bind(this));
  108. html.find('input,select').on('change', this._onInputChange.bind(this));
  109. html.find('textarea').on('change', this._onInputChange.bind(this));
  110. const parentId = html.find('[name="parentID"]');
  111. parentId.on('change', (event) => {
  112. if (event.target.value === 'TOKEN') {
  113. html.find('.token-specific-fields').show();
  114. } else {
  115. html.find('.token-specific-fields').hide();
  116. }
  117. this.setPosition();
  118. });
  119. parentId.trigger('change');
  120. html
  121. .find('[name="ui"]')
  122. .on('change', (event) => {
  123. if (parentId.val() === 'TOKEN') {
  124. if (event.target.checked) {
  125. html.find('.ui-hide').hide();
  126. } else {
  127. html.find('.ui-hide').show();
  128. }
  129. this.setPosition();
  130. }
  131. })
  132. .trigger('change');
  133. html.find('[name="filter"]').on('change', (event) => {
  134. html.find('.filterOptions').empty();
  135. const filterOptions = $(genFilterOptionControls(event.target.value));
  136. html.find('.filterOptions').append(filterOptions);
  137. this.setPosition({ height: 'auto' });
  138. this.activateListeners(filterOptions);
  139. });
  140. html.find('.token-variants-image-select-button').click((event) => {
  141. showArtSelect(this.token?.name ?? 'overlay', {
  142. searchType: SEARCH_TYPE.TOKEN,
  143. callback: (imgSrc, imgName) => {
  144. if (imgSrc) $(event.target).closest('.form-group').find('input').val(imgSrc).trigger('change');
  145. },
  146. });
  147. });
  148. html.find('.presetImport').on('click', (event) => {
  149. const presetName = $(event.target).closest('.form-group').find('.tmfxPreset').val();
  150. if (presetName) {
  151. const preset = TokenMagic.getPreset(presetName);
  152. if (preset) {
  153. $(event.target).closest('.form-group').find('textarea').val(JSON.stringify(preset, null, 2)).trigger('input');
  154. }
  155. }
  156. });
  157. // Controls for locking scale sliders together
  158. let scaleState = { locked: true };
  159. // Range inputs need to be triggered when slider moves to initiate preview
  160. html
  161. .find('.range-value')
  162. .siblings('[type="range"]')
  163. .on('change', (event) => {
  164. $(event.target).siblings('.range-value').val(event.target.value).trigger('change');
  165. });
  166. const lockButtons = $(html).find('.scaleLock > a');
  167. const sliderScaleWidth = $(html).find('[name="scaleX"]');
  168. const sliderScaleHeight = $(html).find('[name="scaleY"]');
  169. const sliderWidth = html.find('.scaleX');
  170. const sliderHeight = html.find('.scaleY');
  171. lockButtons.on('click', function () {
  172. scaleState.locked = !scaleState.locked;
  173. lockButtons.html(scaleState.locked ? '<i class="fas fa-link"></i>' : '<i class="fas fa-unlink"></i>');
  174. });
  175. sliderScaleWidth.on('change', function () {
  176. if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) {
  177. sliderScaleHeight.val(sliderScaleWidth.val()).trigger('change');
  178. sliderHeight.val(sliderScaleWidth.val());
  179. }
  180. });
  181. sliderScaleHeight.on('change', function () {
  182. if (scaleState.locked && sliderScaleWidth.val() !== sliderScaleHeight.val()) {
  183. sliderScaleWidth.val(sliderScaleHeight.val()).trigger('change');
  184. sliderWidth.val(sliderScaleHeight.val());
  185. }
  186. });
  187. html.on('change', '.scaleX', () => {
  188. sliderScaleWidth.trigger('change');
  189. });
  190. html.on('change', '.scaleY', () => {
  191. sliderScaleHeight.trigger('change');
  192. });
  193. html.find('.me-edit-json').on('click', async (event) => {
  194. const textarea = $(event.target).closest('.form-group').find('textarea');
  195. let params;
  196. try {
  197. params = eval(textarea.val());
  198. } catch (e) {
  199. console.warn('TVA |', e);
  200. }
  201. if (params) {
  202. let param;
  203. if (Array.isArray(params)) {
  204. if (params.length === 1) param = params[0];
  205. else {
  206. let i = await promptParamChoice(params);
  207. if (i < 0) return;
  208. param = params[i];
  209. }
  210. } else {
  211. param = params;
  212. }
  213. if (param)
  214. game.modules.get('multi-token-edit').api.showGenericForm(param, param.filterType ?? 'TMFX', {
  215. inputChangeCallback: (selected) => {
  216. mergeObject(param, selected, { inplace: true });
  217. textarea.val(JSON.stringify(params, null, 2)).trigger('input');
  218. },
  219. });
  220. }
  221. });
  222. const underlay = html.find('[name="underlay"]');
  223. const top = html.find('[name="top"]');
  224. const bottom = html.find('[name="bottom"]');
  225. underlay.change(function () {
  226. if (this.checked) top.prop('checked', false);
  227. else bottom.prop('checked', false);
  228. });
  229. top.change(function () {
  230. if (this.checked) {
  231. underlay.prop('checked', false);
  232. bottom.prop('checked', false);
  233. }
  234. });
  235. bottom.change(function () {
  236. if (this.checked) {
  237. underlay.prop('checked', true);
  238. top.prop('checked', false);
  239. }
  240. });
  241. const linkScale = html.find('[name="linkScale"]');
  242. const linkDimensions = html.find('[name="linkDimensionsX"], [name="linkDimensionsY"]');
  243. const linkStageScale = html.find('[name="linkStageScale"]');
  244. linkScale.change(function () {
  245. if (this.checked) {
  246. linkDimensions.prop('checked', false);
  247. linkStageScale.prop('checked', false);
  248. }
  249. });
  250. linkDimensions.change(function () {
  251. if (this.checked) {
  252. linkScale.prop('checked', false);
  253. linkStageScale.prop('checked', false);
  254. }
  255. });
  256. linkStageScale.change(function () {
  257. if (this.checked) {
  258. linkScale.prop('checked', false);
  259. linkDimensions.prop('checked', false);
  260. }
  261. });
  262. // Setting border color for property expression
  263. const limitOnProperty = html.find('[name="limitOnProperty"]');
  264. limitOnProperty.on('input', (event) => {
  265. const input = $(event.target);
  266. if (input.val() === '') {
  267. input.removeClass('tvaValid');
  268. input.removeClass('tvaInvalid');
  269. } else if (input.val().match(VALID_EXPRESSION)) {
  270. input.addClass('tvaValid');
  271. input.removeClass('tvaInvalid');
  272. } else {
  273. input.addClass('tvaInvalid');
  274. input.removeClass('tvaValid');
  275. }
  276. });
  277. limitOnProperty.trigger('input');
  278. html.find('.create-variable').on('click', this._onCreateVariable.bind(this));
  279. html.find('.delete-variable').on('click', this._onDeleteVariable.bind(this));
  280. }
  281. _onDeleteVariable(event) {
  282. let index = $(event.target).closest('tr').data('index');
  283. if (index != null) {
  284. this.config = this._getSubmitData();
  285. if (!this.config.variables) this.config.variables = [];
  286. this.config.variables.splice(index, 1);
  287. this.render(true);
  288. }
  289. }
  290. _onCreateVariable(event) {
  291. this.config = this._getSubmitData();
  292. if (!this.config.variables) this.config.variables = [];
  293. this.config.variables.push({ name: '', value: '' });
  294. this.render(true);
  295. }
  296. _onAddShape(event) {
  297. let shape = $(event.target).siblings('select').val();
  298. shape = deepClone(OVERLAY_SHAPES[shape]);
  299. shape = mergeObject(deepClone(CORE_SHAPE), { shape });
  300. this.config = this._getSubmitData();
  301. if (!this.config.shapes) this.config.shapes = [];
  302. this.config.shapes.push(shape);
  303. this.render(true);
  304. }
  305. _onAddEvent(event) {
  306. let listener = $(event.target).siblings('select').val();
  307. this.config = this._getSubmitData();
  308. if (!this.config.interactivity) this.config.interactivity = [];
  309. this.config.interactivity.push({ listener, macro: '', script: '' });
  310. this.render(true);
  311. }
  312. _onDeleteShape(event) {
  313. const index = $(event.target).closest('.deleteShape').data('index');
  314. if (index == null) return;
  315. this.config = this._getSubmitData();
  316. if (!this.config.shapes) this.config.shapes = [];
  317. this.config.shapes.splice(index, 1);
  318. this.render(true);
  319. }
  320. _onDeleteEvent(event) {
  321. const index = $(event.target).closest('.deleteEvent').data('index');
  322. if (index == null) return;
  323. this.config = this._getSubmitData();
  324. if (!this.config.interactivity) this.config.interactivity = [];
  325. this.config.interactivity.splice(index, 1);
  326. this.render(true);
  327. }
  328. _onCloneShape(event) {
  329. const index = $(event.target).closest('.cloneShape').data('index');
  330. if (!index && index != 0) return;
  331. this.config = this._getSubmitData();
  332. if (!this.config.shapes) return;
  333. const nShape = deepClone(this.config.shapes[index]);
  334. if (nShape.label) {
  335. nShape.label = nShape.label + ' - Copy';
  336. }
  337. this.config.shapes.push(nShape);
  338. this.render(true);
  339. }
  340. _onMoveShapeUp(event) {
  341. const index = $(event.target).closest('.moveShapeUp').data('index');
  342. if (!index) return;
  343. this.config = this._getSubmitData();
  344. if (!this.config.shapes) this.config.shapes = [];
  345. if (this.config.shapes.length >= 2) this._swapShapes(index, index - 1);
  346. this.render(true);
  347. }
  348. _onMoveShapeDown(event) {
  349. const index = $(event.target).closest('.moveShapeDown').data('index');
  350. if (!index && index != 0) return;
  351. this.config = this._getSubmitData();
  352. if (!this.config.shapes) this.config.shapes = [];
  353. if (this.config.shapes.length >= 2) this._swapShapes(index, index + 1);
  354. this.render(true);
  355. }
  356. _swapShapes(i1, i2) {
  357. let temp = this.config.shapes[i1];
  358. this.config.shapes[i1] = this.config.shapes[i2];
  359. this.config.shapes[i2] = temp;
  360. }
  361. _convertColor(colString) {
  362. try {
  363. const c = Color.fromString(colString);
  364. const rgba = c.rgb;
  365. rgba.push(1);
  366. return rgba;
  367. } catch (e) {
  368. return [1, 1, 1, 1];
  369. }
  370. }
  371. async _onInputChange(event) {
  372. this.previewConfig = this._getSubmitData();
  373. if (event.target.type === 'color') {
  374. const color = $(event.target).siblings('.color');
  375. color.val(event.target.value).trigger('change');
  376. return;
  377. }
  378. this._applyPreviews();
  379. }
  380. getPreviewIcons() {
  381. if (!this.config.id) return [];
  382. const tokens = this.token ? [this.token] : canvas.tokens.placeables;
  383. const previewIcons = [];
  384. for (const tkn of tokens) {
  385. if (tkn.tvaOverlays) {
  386. for (const c of tkn.tvaOverlays) {
  387. if (c.overlayConfig && c.overlayConfig.id === this.config.id) {
  388. // Effect icon found, however if we're in global preview then we need to take into account
  389. // a token/actor specific mapping which may override the global one
  390. if (this.token) {
  391. previewIcons.push({ token: tkn, icon: c });
  392. } else if (!getFlagMappings(tkn).find((m) => m.id === this.config.id)) {
  393. previewIcons.push({ token: tkn, icon: c });
  394. }
  395. }
  396. }
  397. }
  398. }
  399. return previewIcons;
  400. }
  401. async _applyPreviews() {
  402. const targets = this.getPreviewIcons();
  403. for (const target of targets) {
  404. const preview = evaluateOverlayExpressions(deepClone(this.previewConfig), target.token, {
  405. overlayConfig: this.previewConfig,
  406. });
  407. target.icon.refresh(preview, {
  408. preview: true,
  409. previewTexture: await genTexture(target.token, preview),
  410. });
  411. }
  412. }
  413. async _removePreviews() {
  414. const targets = this.getPreviewIcons();
  415. for (const target of targets) {
  416. target.icon.refresh();
  417. }
  418. }
  419. async getData(options) {
  420. const data = super.getData(options);
  421. data.filters = Object.keys(PIXI.filters);
  422. data.filters.push('OutlineOverlayFilter');
  423. data.filters.sort();
  424. data.tmfxActive = game.modules.get('tokenmagic')?.active;
  425. if (data.tmfxActive) {
  426. data.tmfxPresets = TokenMagic.getPresets().map((p) => p.name);
  427. data.filters.unshift('Token Magic FX');
  428. }
  429. data.filters.unshift('NONE');
  430. const settings = mergeObject(DEFAULT_OVERLAY_CONFIG, this.config, {
  431. inplace: false,
  432. });
  433. data.ceActive = game.modules.get('dfreds-convenient-effects')?.active;
  434. if (data.ceActive) {
  435. data.ceEffects = game.dfreds.effects.all.map((ef) => ef.name);
  436. }
  437. data.macros = game.macros.map((m) => m.name);
  438. if (settings.filter !== 'NONE') {
  439. const filterOptions = genFilterOptionControls(settings.filter, settings.filterOptions);
  440. if (filterOptions) {
  441. settings.filterOptions = filterOptions;
  442. } else {
  443. settings.filterOptions = null;
  444. }
  445. } else {
  446. settings.filterOptions = null;
  447. }
  448. data.users = game.users.map((u) => {
  449. return { id: u.id, name: u.name, selected: settings.limitedUsers.includes(u.id) };
  450. });
  451. data.fonts = Object.keys(CONFIG.fontDefinitions);
  452. const allMappings = getAllEffectMappings(this.token, true).filter((m) => m.id !== this.config.id);
  453. const [_, groupedMappings] = sortMappingsToGroups(allMappings);
  454. data.parents = groupedMappings;
  455. if (!data.parentID) data.parentID = 'TOKEN';
  456. if (!data.anchor) data.anchor = { x: 0.5, y: 0.5 };
  457. // Cache Partials
  458. for (const shapeName of Object.keys(OVERLAY_SHAPES)) {
  459. await getTemplate(`modules/token-variants/templates/partials/shape${shapeName}.html`);
  460. }
  461. await getTemplate('modules/token-variants/templates/partials/repeating.html');
  462. await getTemplate('modules/token-variants/templates/partials/interpolateColor.html');
  463. data.allShapes = Object.keys(OVERLAY_SHAPES);
  464. data.textAlignmentOptions = [
  465. { value: 'left', label: 'Left' },
  466. { value: 'center', label: 'Center' },
  467. { value: 'right', label: 'Right' },
  468. { value: 'justify', label: 'Justify' },
  469. ];
  470. // linkDimensions has been converted to linkDimensionsX and linkDimensionsY
  471. // Make sure we're using the latest fields
  472. // 20/07/2023
  473. if (!('linkDimensionsX' in settings) && settings.linkDimensions) {
  474. settings.linkDimensionsX = true;
  475. settings.linkDimensionsY = true;
  476. }
  477. return mergeObject(data, settings);
  478. }
  479. _getHeaderButtons() {
  480. const buttons = super._getHeaderButtons();
  481. buttons.unshift({
  482. label: 'Core Variables',
  483. class: '.core-variables',
  484. icon: 'fas fa-file-import fa-fw',
  485. onclick: () => {
  486. let content = `
  487. <table>
  488. <tr><th>Variable</th><th>Description</th></tr>
  489. <tr><td>@hp</td><td>Actor Health</td></tr>
  490. <tr><td>@hpMax</td><td>Actor Health (Max)</td></tr>
  491. <tr><td>@gridSize</td><td>Grid Size (Pixels)</td></tr>
  492. <tr><td>@label</td><td>Mapping's Label Field</td></tr>
  493. </table>
  494. `;
  495. new Dialog({
  496. title: `Core Variables`,
  497. content,
  498. buttons: {},
  499. }).render(true);
  500. },
  501. });
  502. return buttons;
  503. }
  504. async close(options = {}) {
  505. super.close(options);
  506. this._removePreviews();
  507. }
  508. _getSubmitData() {
  509. let formData = super._getSubmitData();
  510. formData = expandObject(formData);
  511. if (!formData.repeating) delete formData.repeat;
  512. if (!formData.text.repeating) delete formData.text.repeat;
  513. if (formData.shapes) {
  514. formData.shapes = Object.values(formData.shapes);
  515. formData.shapes.forEach((shape) => {
  516. if (!shape.repeating) delete shape.repeat;
  517. });
  518. }
  519. if (formData.interactivity) {
  520. formData.interactivity = Object.values(formData.interactivity)
  521. .map((e) => {
  522. e.macro = e.macro.trim();
  523. e.script = e.script.trim();
  524. if (e.tmfxPreset) e.tmfxPreset = e.tmfxPreset.trim();
  525. if (e.ceEffect) e.ceEffect = e.ceEffect.trim();
  526. return e;
  527. })
  528. .filter((e) => e.macro || e.script || e.ceEffect || e.tmfxPreset);
  529. } else {
  530. formData.interactivity = [];
  531. }
  532. if (formData.variables) {
  533. formData.variables = Object.values(formData.variables);
  534. formData.variables = formData.variables.filter((v) => v.name.trim() && v.value.trim());
  535. }
  536. if (formData.limitedUsers) {
  537. if (getType(formData.limitedUsers) === 'string') formData.limitedUsers = [formData.limitedUsers];
  538. formData.limitedUsers = formData.limitedUsers.filter((uid) => uid);
  539. } else {
  540. formData.limitedUsers = [];
  541. }
  542. formData.limitOnEffect = formData.limitOnEffect.trim();
  543. formData.limitOnProperty = formData.limitOnProperty.trim();
  544. if (formData.parentID === 'TOKEN') formData.parentID = '';
  545. if (formData.filter === 'OutlineOverlayFilter' && 'filterOptions.outlineColor' in formData) {
  546. formData['filterOptions.outlineColor'] = this._convertColor(formData['filterOptions.outlineColor']);
  547. } else if (formData.filter === 'BevelFilter') {
  548. if ('filterOptions.lightColor' in formData)
  549. formData['filterOptions.lightColor'] = Number(Color.fromString(formData['filterOptions.lightColor']));
  550. if ('filterOptions.shadowColor' in formData)
  551. formData['filterOptions.shadowColor'] = Number(Color.fromString(formData['filterOptions.shadowColor']));
  552. } else if (['DropShadowFilter', 'GlowFilter', 'OutlineFilter', 'FilterFire'].includes(formData.filter)) {
  553. if ('filterOptions.color' in formData)
  554. formData['filterOptions.color'] = Number(Color.fromString(formData['filterOptions.color']));
  555. }
  556. return formData;
  557. }
  558. /**
  559. * @param {Event} event
  560. * @param {Object} formData
  561. */
  562. async _updateObject(event, formData) {
  563. if (this.callback) this.callback(formData);
  564. }
  565. }
  566. export const FILTERS = {
  567. OutlineOverlayFilter: {
  568. defaultValues: {
  569. outlineColor: [0, 0, 0, 1],
  570. trueThickness: 1,
  571. animate: false,
  572. },
  573. controls: [
  574. {
  575. type: 'color',
  576. name: 'outlineColor',
  577. },
  578. {
  579. type: 'range',
  580. label: 'Thickness',
  581. name: 'trueThickness',
  582. min: 0,
  583. max: 5,
  584. step: 0.01,
  585. },
  586. {
  587. type: 'boolean',
  588. label: 'Oscillate',
  589. name: 'animate',
  590. },
  591. ],
  592. argType: 'args',
  593. },
  594. AlphaFilter: {
  595. defaultValues: {
  596. alpha: 1,
  597. },
  598. controls: [
  599. {
  600. type: 'range',
  601. name: 'alpha',
  602. min: 0,
  603. max: 1,
  604. step: 0.01,
  605. },
  606. ],
  607. argType: 'args',
  608. },
  609. BlurFilter: {
  610. defaultValues: {
  611. strength: 8,
  612. quality: 4,
  613. },
  614. controls: [
  615. { type: 'range', name: 'strength', min: 0, max: 20, step: 1 },
  616. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  617. ],
  618. argType: 'args',
  619. },
  620. BlurFilterPass: {
  621. defaultValues: {
  622. horizontal: false,
  623. strength: 8,
  624. quality: 4,
  625. },
  626. controls: [
  627. {
  628. type: 'boolean',
  629. name: 'horizontal',
  630. },
  631. { type: 'range', name: 'strength', min: 0, max: 20, step: 1 },
  632. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  633. ],
  634. argType: 'args',
  635. },
  636. NoiseFilter: {
  637. defaultValues: {
  638. noise: 0.5,
  639. seed: 4475160954091,
  640. },
  641. controls: [
  642. { type: 'range', name: 'noise', min: 0, max: 1, step: 0.01 },
  643. { type: 'range', name: 'seed', min: 0, max: 100000, step: 1 },
  644. ],
  645. argType: 'args',
  646. },
  647. AdjustmentFilter: {
  648. defaultValues: {
  649. gamma: 1,
  650. saturation: 1,
  651. contrast: 1,
  652. brightness: 1,
  653. red: 1,
  654. green: 1,
  655. blue: 1,
  656. alpha: 1,
  657. },
  658. controls: [
  659. { type: 'range', name: 'gamma', min: 0, max: 1, step: 0.01 },
  660. { type: 'range', name: 'saturation', min: 0, max: 1, step: 0.01 },
  661. { type: 'range', name: 'contrast', min: 0, max: 1, step: 0.01 },
  662. { type: 'range', name: 'brightness', min: 0, max: 1, step: 0.01 },
  663. { type: 'range', name: 'red', min: 0, max: 1, step: 0.01 },
  664. { type: 'range', name: 'green', min: 0, max: 1, step: 0.01 },
  665. { type: 'range', name: 'blue', min: 0, max: 1, step: 0.01 },
  666. { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
  667. ],
  668. argType: 'options',
  669. },
  670. AdvancedBloomFilter: {
  671. defaultValues: {
  672. threshold: 0.5,
  673. bloomScale: 1,
  674. brightness: 1,
  675. blur: 8,
  676. quality: 4,
  677. },
  678. controls: [
  679. { type: 'range', name: 'threshold', min: 0, max: 1, step: 0.01 },
  680. { type: 'range', name: 'bloomScale', min: 0, max: 5, step: 0.01 },
  681. { type: 'range', name: 'brightness', min: 0, max: 1, step: 0.01 },
  682. { type: 'range', name: 'blur', min: 0, max: 20, step: 1 },
  683. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  684. ],
  685. argType: 'options',
  686. },
  687. AsciiFilter: {
  688. defaultValues: {
  689. size: 8,
  690. },
  691. controls: [{ type: 'range', name: 'size', min: 0, max: 20, step: 0.01 }],
  692. argType: 'args',
  693. },
  694. BevelFilter: {
  695. defaultValues: {
  696. rotation: 45,
  697. thickness: 2,
  698. lightColor: 0xffffff,
  699. lightAlpha: 0.7,
  700. shadowColor: 0x000000,
  701. shadowAlpha: 0.7,
  702. },
  703. controls: [
  704. { type: 'range', name: 'rotation', min: 0, max: 360, step: 1 },
  705. { type: 'range', name: 'thickness', min: 0, max: 20, step: 0.01 },
  706. { type: 'color', name: 'lightColor' },
  707. { type: 'range', name: 'lightAlpha', min: 0, max: 1, step: 0.01 },
  708. { type: 'color', name: 'shadowColor' },
  709. { type: 'range', name: 'shadowAlpha', min: 0, max: 1, step: 0.01 },
  710. ],
  711. argType: 'options',
  712. },
  713. BloomFilter: {
  714. defaultValues: {
  715. blur: 2,
  716. quality: 4,
  717. },
  718. controls: [
  719. { type: 'range', name: 'blur', min: 0, max: 20, step: 1 },
  720. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  721. ],
  722. argType: 'args',
  723. },
  724. BulgePinchFilter: {
  725. defaultValues: {
  726. radius: 100,
  727. strength: 1,
  728. },
  729. controls: [
  730. { type: 'range', name: 'radius', min: 0, max: 500, step: 1 },
  731. { type: 'range', name: 'strength', min: -1, max: 1, step: 0.01 },
  732. ],
  733. argType: 'options',
  734. },
  735. CRTFilter: {
  736. defaultValues: {
  737. curvature: 1,
  738. lineWidth: 1,
  739. lineContrast: 0.25,
  740. verticalLine: false,
  741. noise: 0.3,
  742. noiseSize: 1,
  743. seed: 0,
  744. vignetting: 0.3,
  745. vignettingAlpha: 1,
  746. vignettingBlur: 0.3,
  747. time: 0,
  748. },
  749. controls: [
  750. { type: 'range', name: 'curvature', min: 0, max: 20, step: 0.01 },
  751. { type: 'range', name: 'lineWidth', min: 0, max: 20, step: 0.01 },
  752. { type: 'range', name: 'lineContrast', min: 0, max: 5, step: 0.01 },
  753. { type: 'boolean', name: 'verticalLine' },
  754. { type: 'range', name: 'noise', min: 0, max: 2, step: 0.01 },
  755. { type: 'range', name: 'noiseSize', min: 0, max: 20, step: 0.01 },
  756. { type: 'range', name: 'seed', min: 0, max: 100000, step: 1 },
  757. { type: 'range', name: 'vignetting', min: 0, max: 20, step: 0.01 },
  758. { type: 'range', name: 'vignettingAlpha', min: 0, max: 1, step: 0.01 },
  759. { type: 'range', name: 'vignettingBlur', min: 0, max: 5, step: 0.01 },
  760. { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
  761. ],
  762. argType: 'options',
  763. },
  764. DotFilter: {
  765. defaultValues: {
  766. scale: 1,
  767. angle: 5,
  768. },
  769. controls: [
  770. { type: 'range', name: 'scale', min: 0, max: 50, step: 1 },
  771. { type: 'range', name: 'angle', min: 0, max: 360, step: 0.1 },
  772. ],
  773. argType: 'args',
  774. },
  775. DropShadowFilter: {
  776. defaultValues: {
  777. rotation: 45,
  778. distance: 5,
  779. color: 0x000000,
  780. alpha: 0.5,
  781. shadowOnly: false,
  782. blur: 2,
  783. quality: 3,
  784. },
  785. controls: [
  786. { type: 'range', name: 'rotation', min: 0, max: 360, step: 0.1 },
  787. { type: 'range', name: 'distance', min: 0, max: 100, step: 0.1 },
  788. { type: 'color', name: 'color' },
  789. { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
  790. { type: 'boolean', name: 'shadowOnly' },
  791. { type: 'range', name: 'blur', min: 0, max: 20, step: 0.1 },
  792. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  793. ],
  794. argType: 'options',
  795. },
  796. EmbossFilter: {
  797. defaultValues: {
  798. strength: 5,
  799. },
  800. controls: [{ type: 'range', name: 'strength', min: 0, max: 20, step: 1 }],
  801. argType: 'args',
  802. },
  803. GlitchFilter: {
  804. defaultValues: {
  805. slices: 5,
  806. offset: 100,
  807. direction: 0,
  808. fillMode: 0,
  809. seed: 0,
  810. average: false,
  811. minSize: 8,
  812. sampleSize: 512,
  813. },
  814. controls: [
  815. { type: 'range', name: 'slices', min: 0, max: 50, step: 1 },
  816. { type: 'range', name: 'distance', min: 0, max: 1000, step: 1 },
  817. { type: 'range', name: 'direction', min: 0, max: 360, step: 0.1 },
  818. {
  819. type: 'select',
  820. name: 'fillMode',
  821. options: [
  822. { value: 0, label: 'TRANSPARENT' },
  823. { value: 1, label: 'ORIGINAL' },
  824. { value: 2, label: 'LOOP' },
  825. { value: 3, label: 'CLAMP' },
  826. { value: 4, label: 'MIRROR' },
  827. ],
  828. },
  829. { type: 'range', name: 'seed', min: 0, max: 10000, step: 1 },
  830. { type: 'boolean', name: 'average' },
  831. { type: 'range', name: 'minSize', min: 0, max: 500, step: 1 },
  832. { type: 'range', name: 'sampleSize', min: 0, max: 1024, step: 1 },
  833. ],
  834. argType: 'options',
  835. },
  836. GlowFilter: {
  837. defaultValues: {
  838. distance: 10,
  839. outerStrength: 4,
  840. innerStrength: 0,
  841. color: 0xffffff,
  842. quality: 0.1,
  843. knockout: false,
  844. },
  845. controls: [
  846. { type: 'range', name: 'distance', min: 1, max: 50, step: 1 },
  847. { type: 'range', name: 'outerStrength', min: 0, max: 20, step: 1 },
  848. { type: 'range', name: 'innerStrength', min: 0, max: 20, step: 1 },
  849. { type: 'color', name: 'color' },
  850. { type: 'range', name: 'quality', min: 0, max: 5, step: 0.1 },
  851. { type: 'boolean', name: 'knockout' },
  852. ],
  853. argType: 'options',
  854. },
  855. GodrayFilter: {
  856. defaultValues: {
  857. angle: 30,
  858. gain: 0.5,
  859. lacunarity: 2.5,
  860. parallel: true,
  861. time: 0,
  862. alpha: 1.0,
  863. },
  864. controls: [
  865. { type: 'range', name: 'angle', min: 0, max: 360, step: 0.1 },
  866. { type: 'range', name: 'gain', min: 0, max: 5, step: 0.01 },
  867. { type: 'range', name: 'lacunarity', min: 0, max: 5, step: 0.01 },
  868. { type: 'boolean', name: 'parallel' },
  869. { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
  870. { type: 'range', name: 'alpha', min: 0, max: 1, step: 0.01 },
  871. ],
  872. argType: 'options',
  873. },
  874. KawaseBlurFilter: {
  875. defaultValues: {
  876. blur: 4,
  877. quality: 3,
  878. clamp: false,
  879. },
  880. controls: [
  881. { type: 'range', name: 'blur', min: 0, max: 20, step: 0.1 },
  882. { type: 'range', name: 'quality', min: 0, max: 20, step: 1 },
  883. { type: 'boolean', name: 'clamp' },
  884. ],
  885. argType: 'args',
  886. },
  887. OldFilmFilter: {
  888. defaultValues: {
  889. sepia: 0.3,
  890. noise: 0.3,
  891. noiseSize: 1.0,
  892. scratch: 0.5,
  893. scratchDensity: 0.3,
  894. scratchWidth: 1.0,
  895. vignetting: 0.3,
  896. vignettingAlpha: 1.0,
  897. vignettingBlur: 0.3,
  898. },
  899. controls: [
  900. { type: 'range', name: 'sepia', min: 0, max: 1, step: 0.01 },
  901. { type: 'range', name: 'noise', min: 0, max: 1, step: 0.01 },
  902. { type: 'range', name: 'noiseSize', min: 0, max: 5, step: 0.01 },
  903. { type: 'range', name: 'scratch', min: 0, max: 5, step: 0.01 },
  904. { type: 'range', name: 'scratchDensity', min: 0, max: 5, step: 0.01 },
  905. { type: 'range', name: 'scratchWidth', min: 0, max: 20, step: 0.01 },
  906. { type: 'range', name: 'vignetting', min: 0, max: 1, step: 0.01 },
  907. { type: 'range', name: 'vignettingAlpha', min: 0, max: 1, step: 0.01 },
  908. { type: 'range', name: 'vignettingBlur', min: 0, max: 5, step: 0.01 },
  909. ],
  910. argType: 'options',
  911. },
  912. OutlineFilter: {
  913. defaultValues: {
  914. thickness: 1,
  915. color: 0x000000,
  916. quality: 0.1,
  917. },
  918. controls: [
  919. { type: 'range', name: 'thickness', min: 0, max: 20, step: 0.1 },
  920. { type: 'color', name: 'color' },
  921. { type: 'range', name: 'quality', min: 0, max: 1, step: 0.01 },
  922. ],
  923. argType: 'args',
  924. },
  925. PixelateFilter: {
  926. defaultValues: {
  927. size: 1,
  928. },
  929. controls: [{ type: 'range', name: 'size', min: 1, max: 100, step: 1 }],
  930. argType: 'args',
  931. },
  932. RGBSplitFilter: {
  933. defaultValues: {
  934. red: [-10, 0],
  935. green: [0, 10],
  936. blue: [0, 0],
  937. },
  938. controls: [
  939. { type: 'point', name: 'red', min: 0, max: 50, step: 1 },
  940. { type: 'point', name: 'green', min: 0, max: 50, step: 1 },
  941. { type: 'point', name: 'blue', min: 0, max: 50, step: 1 },
  942. ],
  943. argType: 'args',
  944. },
  945. RadialBlurFilter: {
  946. defaultValues: {
  947. angle: 0,
  948. center: [0, 0],
  949. radius: -1,
  950. },
  951. controls: [
  952. { type: 'range', name: 'angle', min: 0, max: 360, step: 1 },
  953. { type: 'point', name: 'center', min: 0, max: 1000, step: 1 },
  954. { type: 'range', name: 'radius', min: -1, max: 1000, step: 1 },
  955. ],
  956. argType: 'args',
  957. },
  958. ReflectionFilter: {
  959. defaultValues: {
  960. mirror: true,
  961. boundary: 0.5,
  962. amplitude: [0, 20],
  963. waveLength: [30, 100],
  964. alpha: [1, 1],
  965. time: 0,
  966. },
  967. controls: [
  968. { type: 'boolean', name: 'mirror' },
  969. { type: 'range', name: 'boundary', min: 0, max: 1, step: 0.01 },
  970. { type: 'point', name: 'amplitude', min: 0, max: 100, step: 1 },
  971. { type: 'point', name: 'waveLength', min: 0, max: 500, step: 1 },
  972. { type: 'point', name: 'alpha', min: 0, max: 1, step: 0.01 },
  973. { type: 'range', name: 'time', min: 0, max: 10000, step: 1 },
  974. ],
  975. argType: 'options',
  976. },
  977. DisplacementFilter: {
  978. defaultValues: {
  979. sprite: '',
  980. textureScale: 1,
  981. displacementScale: 1,
  982. },
  983. controls: [
  984. { type: 'text', name: 'sprite' },
  985. { type: 'range', name: 'textureScale', min: 0, max: 100, step: 0.1 },
  986. { type: 'range', name: 'displacementScale', min: 0, max: 100, step: 0.1 },
  987. ],
  988. argType: 'options',
  989. },
  990. 'Token Magic FX': {
  991. defaultValues: {
  992. params: [],
  993. },
  994. controls: [
  995. { type: 'tmfxPreset', name: 'tmfxPreset' },
  996. { type: 'json', name: 'params' },
  997. ],
  998. },
  999. };
  1000. function genFilterOptionControls(filterName, filterOptions = {}) {
  1001. if (!(filterName in FILTERS)) return;
  1002. const options = mergeObject(FILTERS[filterName].defaultValues, filterOptions);
  1003. const values = getControlValues(filterName, options);
  1004. const controls = FILTERS[filterName].controls;
  1005. let controlsHTML = '<fieldset><legend>Options</legend>';
  1006. for (const control of controls) {
  1007. controlsHTML += genControl(control, values);
  1008. }
  1009. controlsHTML += '</fieldset>';
  1010. return controlsHTML;
  1011. }
  1012. function getControlValues(filterName, options) {
  1013. if (filterName === 'OutlineOverlayFilter') {
  1014. options.outlineColor = Color.fromRGB(options.outlineColor).toString();
  1015. } else if (filterName === 'BevelFilter') {
  1016. options.lightColor = Color.from(options.lightColor).toString();
  1017. options.shadowColor = Color.from(options.shadowColor).toString();
  1018. } else if (['DropShadowFilter', 'GlowFilter', 'OutlineFilter'].includes(filterName)) {
  1019. options.color = Color.from(options.color).toString();
  1020. }
  1021. return options;
  1022. }
  1023. function genControl(control, values) {
  1024. const val = values[control.name];
  1025. const name = control.name;
  1026. const label = control.label ?? name.charAt(0).toUpperCase() + name.slice(1);
  1027. const type = control.type;
  1028. if (type === 'color') {
  1029. return `
  1030. <div class="form-group">
  1031. <label>${label}</label>
  1032. <div class="form-fields">
  1033. <input class="color" type="text" name="filterOptions.${name}" value="${val}">
  1034. <input type="color" value="${val}" data-edit="filterOptions.${name}">
  1035. </div>
  1036. </div>
  1037. `;
  1038. } else if (type === 'range') {
  1039. return `
  1040. <div class="form-group">
  1041. <label>${label}</label>
  1042. <div class="form-fields">
  1043. <input type="range" name="filterOptions.${name}" value="${val}" min="${control.min}" max="${control.max}" step="${control.step}">
  1044. <span class="range-value">${val}</span>
  1045. </div>
  1046. </div>
  1047. `;
  1048. } else if (type === 'boolean') {
  1049. return `
  1050. <div class="form-group">
  1051. <label>${label}</label>
  1052. <div class="form-fields">
  1053. <input type="checkbox" name="filterOptions.${name}" data-dtype="Boolean" value="${val}" ${val ? 'checked' : ''}>
  1054. </div>
  1055. </div>
  1056. `;
  1057. } else if (type === 'select') {
  1058. let select = `
  1059. <div class="form-group">
  1060. <label>${label}</label>
  1061. <div class="form-fields">
  1062. <select name="${name}">
  1063. `;
  1064. for (const opt of control.options) {
  1065. select += `<option value="${opt.value}" ${val === opt.value ? 'selected="selected"' : ''}>${opt.label}</option>`;
  1066. }
  1067. select += `</select></div></div>`;
  1068. return select;
  1069. } else if (type === 'point') {
  1070. return `
  1071. <div class="form-group">
  1072. <label>${label}</label>
  1073. <div class="form-fields">
  1074. <input type="range" name="filterOptions.${name}" value="${val[0]}" min="${control.min}" max="${control.max}" step="${control.step}">
  1075. <span class="range-value">${val[0]}</span>
  1076. </div>
  1077. <div class="form-fields">
  1078. <input type="range" name="filterOptions.${name}" value="${val[1]}" min="${control.min}" max="${control.max}" step="${control.step}">
  1079. <span class="range-value">${val[1]}</span>
  1080. </div>
  1081. </div>
  1082. `;
  1083. } else if (type === 'json') {
  1084. let control = `
  1085. <div class="form-group">
  1086. <label>${label}</label>
  1087. <div class="form-fields">
  1088. <textarea style="width: 450px; height: 200px;" name="filterOptions.${name}">${val}</textarea>
  1089. </div>`;
  1090. if (game.modules.get('multi-token-edit')?.api.showGenericForm) {
  1091. control += `
  1092. <div style="text-align: right; color: orangered;">
  1093. <a> <i class="me-edit-json fas fa-edit" title="Show Generic Form"></i></a>
  1094. </div>`;
  1095. }
  1096. control += `</div>`;
  1097. return control;
  1098. } else if (type === 'text') {
  1099. return `
  1100. <div class="form-group">
  1101. <label>${label}</label>
  1102. <div class="form-fields">
  1103. <input type="text" name="filterOptions.${name}" value="${val}">
  1104. </div>
  1105. </div>
  1106. `;
  1107. } else if (type === 'tmfxPreset' && game.modules.get('tokenmagic')?.active) {
  1108. return `
  1109. <div class="form-group">
  1110. <label>Preset <span class="units">(TMFX)</span></label>
  1111. <div class="form-fields">
  1112. <input list="tmfxPresets" class="tmfxPreset">
  1113. <button type="button" class="presetImport"><i class="fas fa-download"></i></button>
  1114. </div>
  1115. `;
  1116. }
  1117. return '';
  1118. }
  1119. async function promptParamChoice(params) {
  1120. return new Promise((resolve, reject) => {
  1121. const buttons = {};
  1122. for (let i = 0; i < params.length; i++) {
  1123. const label = params[i].filterType ?? params[i].filterId;
  1124. buttons[label] = {
  1125. label,
  1126. callback: () => {
  1127. resolve(i);
  1128. },
  1129. };
  1130. }
  1131. const dialog = new Dialog({
  1132. title: 'Select Filter To Edit',
  1133. content: '',
  1134. buttons,
  1135. close: () => resolve(-1),
  1136. });
  1137. dialog.render(true);
  1138. });
  1139. }