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.

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