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.

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