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.

720 lines
25 KiB

  1. import { FILTERS } from '../../applications/overlayConfig.js';
  2. import { evaluateComparator, getTokenEffects } from '../hooks/effectMappingHooks.js';
  3. import { registerOverlayRefreshHook, unregisterOverlayRefreshHooks } from '../hooks/overlayHooks.js';
  4. import { DEFAULT_OVERLAY_CONFIG } from '../models.js';
  5. import { interpolateColor, removeMarkedOverlays } from '../token/overlay.js';
  6. import { executeMacro, toggleCEEffect, toggleTMFXPreset, tv_executeScript } from '../utils.js';
  7. import { HTMLOverlay } from './HTMLOverlay.js';
  8. export class TVAOverlay extends TokenMesh {
  9. constructor(pTexture, token, config) {
  10. super(token);
  11. if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);
  12. this.pseudoTexture = pTexture;
  13. this.texture = pTexture.texture;
  14. //this.setTexture(pTexture, { refresh: false });
  15. this.ready = false;
  16. this.overlaySort = 0;
  17. this.filtersApplied = false;
  18. this.overlayConfig = mergeObject(DEFAULT_OVERLAY_CONFIG, config, { inplace: false });
  19. if (pTexture.html) this.addHTMLOverlay();
  20. // linkDimensions has been converted to linkDimensionsX and linkDimensionsY
  21. // Make sure we're using the latest fields
  22. // 20/07/2023
  23. if (!('linkDimensionsX' in this.overlayConfig) && this.overlayConfig.linkDimensions) {
  24. this.overlayConfig.linkDimensionsX = true;
  25. this.overlayConfig.linkDimensionsY = true;
  26. }
  27. this._registerHooks(this.overlayConfig);
  28. this._tvaPlay().then(() => this.refresh());
  29. // Workaround needed for v11 visible property
  30. Object.defineProperty(this, 'visible', {
  31. get: this._customVisible,
  32. set: function () {},
  33. configurable: true,
  34. });
  35. this.eventMode = 'none';
  36. }
  37. enableInteractivity() {
  38. if (this.mouseInteractionManager && !this.overlayConfig.interactivity?.length) {
  39. this.removeAllListeners();
  40. this.mouseInteractionManager = null;
  41. this.cursor = null;
  42. this.eventMode = 'none';
  43. return;
  44. } else if (this.mouseInteractionManager || !this.overlayConfig.interactivity?.length) return;
  45. if (!this.overlayConfig.ui) {
  46. if (canvas.primary.eventMode === 'passive') {
  47. canvas.primary.eventMode = 'passive';
  48. }
  49. }
  50. this.eventMode = 'static';
  51. // If this overlay iss interactable all of its parents need to be set as interactable too
  52. let parent = this.parent;
  53. while (parent instanceof TVAOverlay || parent?.parent instanceof TVAOverlay) {
  54. parent.eventMode = 'passive';
  55. parent = parent.parent;
  56. }
  57. this.cursor = 'pointer';
  58. const token = this.object;
  59. const sprite = this;
  60. const runInteraction = function (event, listener) {
  61. sprite.overlayConfig.interactivity.forEach((i) => {
  62. if (i.listener === listener) {
  63. event.preventDefault();
  64. event.stopPropagation();
  65. if (i.script) tv_executeScript(i.script, { token });
  66. if (i.macro) executeMacro(i.macro, token);
  67. if (i.ceEffect) toggleCEEffect(token, i.ceEffect);
  68. if (i.tmfxPreset) toggleTMFXPreset(token, i.tmfxPreset);
  69. }
  70. });
  71. };
  72. const permissions = {
  73. hoverIn: () => true,
  74. hoverOut: () => true,
  75. clickLeft: () => true,
  76. clickLeft2: () => true,
  77. clickRight: () => true,
  78. clickRight2: () => true,
  79. dragStart: () => false,
  80. };
  81. const callbacks = {
  82. hoverIn: (event) => runInteraction(event, 'hoverIn'),
  83. hoverOut: (event) => runInteraction(event, 'hoverOut'),
  84. clickLeft: (event) => runInteraction(event, 'clickLeft'),
  85. clickLeft2: (event) => runInteraction(event, 'clickLeft2'),
  86. clickRight: (event) => runInteraction(event, 'clickRight'),
  87. clickRight2: (event) => runInteraction(event, 'clickRight2'),
  88. dragLeftStart: null,
  89. dragLeftMove: null,
  90. dragLeftDrop: null,
  91. dragLeftCancel: null,
  92. dragRightStart: null,
  93. dragRightMove: null,
  94. dragRightDrop: null,
  95. dragRightCancel: null,
  96. longPress: null,
  97. };
  98. const options = { target: null };
  99. // Create the interaction manager
  100. const mgr = new MouseInteractionManager(this, canvas.stage, permissions, callbacks, options);
  101. this.mouseInteractionManager = mgr.activate();
  102. }
  103. _customVisible() {
  104. const ov = this.overlayConfig;
  105. if (!this.ready || !(this.object.visible || ov.alwaysVisible)) return false;
  106. if (ov.limitedToOwner && !this.object.owner) return false;
  107. if (ov.limitedUsers?.length && !ov.limitedUsers.includes(game.user.id)) return false;
  108. if (ov.limitOnEffect || ov.limitOnProperty) {
  109. const speaker = ChatMessage.getSpeaker();
  110. let token = canvas.ready ? canvas.tokens.get(speaker.token) : null;
  111. if (!token) return false;
  112. if (ov.limitOnEffect) {
  113. if (!getTokenEffects(token).includes(ov.limitOnEffect)) return false;
  114. }
  115. if (ov.limitOnProperty) {
  116. if (!evaluateComparator(token.document, ov.limitOnProperty)) return false;
  117. }
  118. }
  119. if (
  120. ov.limitOnHover ||
  121. ov.limitOnControl ||
  122. ov.limitOnHighlight ||
  123. ov.limitOnHUD ||
  124. ov.limitOnTarget ||
  125. ov.limitOnAnyTarget
  126. ) {
  127. if (ov.limitOnHover && canvas.controls.ruler._state === Ruler.STATES.INACTIVE && this.object.hover) return true;
  128. if (ov.limitOnControl && this.object.controlled) return true;
  129. if (ov.limitOnHighlight && (canvas.tokens.highlightObjects ?? canvas.tokens._highlight)) return true;
  130. if (ov.limitOnHUD && this.object.hasActiveHUD) return true;
  131. if (ov.limitOnAnyTarget && this.object.targeted.size) return true;
  132. if (ov.limitOnTarget && this.object.targeted.some((u) => u.id === game.userId)) return true;
  133. return false;
  134. }
  135. return true;
  136. }
  137. // Overlays have the same sort order as the parent
  138. get sort() {
  139. const sort = this.object.mesh.sort;
  140. if (this.overlayConfig.top) return sort + 9999;
  141. else if (this.overlayConfig.bottom) return sort - 9999;
  142. return sort;
  143. }
  144. get _lastSortedIndex() {
  145. return (this.object.mesh._lastSortedIndex || 0) + this.overlaySort;
  146. }
  147. get elevation() {
  148. const elevation = this.object.mesh?.data.elevation;
  149. if (this.overlayConfig.bottom && elevation > 0) return 0;
  150. else if (this.overlayConfig.top) return elevation + 9999;
  151. return elevation;
  152. }
  153. set _lastSortedIndex(val) {}
  154. async _tvaPlay() {
  155. // Ensure playback state for video
  156. const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
  157. if (source && source.tagName === 'VIDEO') {
  158. // Detach video from others
  159. const s = source.cloneNode();
  160. if (this.overlayConfig.playOnce) {
  161. s.onended = () => {
  162. this.alpha = 0;
  163. this.tvaVideoEnded = true;
  164. };
  165. }
  166. await new Promise((resolve) => (s.oncanplay = resolve));
  167. this.texture = PIXI.Texture.from(s, { resourceOptions: { autoPlay: false } });
  168. const options = {
  169. loop: this.overlayConfig.loop && !this.overlayConfig.playOnce,
  170. volume: 0,
  171. offset: 0,
  172. playing: true,
  173. };
  174. game.video.play(s, options);
  175. }
  176. }
  177. addChildAuto(...children) {
  178. if (this.pseudoTexture?.shapes) {
  179. return this.pseudoTexture.shapes.addChild(...children);
  180. } else {
  181. return this.addChild(...children);
  182. }
  183. }
  184. setTexture(pTexture, { preview = false, refresh = true, configuration = null, refreshFilters = false } = {}) {
  185. // Text preview handling
  186. if (preview) {
  187. this._swapChildren(pTexture);
  188. if (this.originalTexture) this._destroyTexture();
  189. else {
  190. this.originalTexture = this.pseudoTexture;
  191. if (this.originalTexture.shapes) this.removeChild(this.originalTexture.shapes);
  192. }
  193. this.pseudoTexture = pTexture;
  194. this.texture = pTexture.texture;
  195. if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);
  196. } else if (this.originalTexture) {
  197. this._swapChildren(this.originalTexture);
  198. this._destroyTexture();
  199. this.pseudoTexture = this.originalTexture;
  200. this.texture = this.originalTexture.texture;
  201. if (this.originalTexture.shapes) this.pseudoTexture.shapes = this.addChild(this.originalTexture.shapes);
  202. delete this.originalTexture;
  203. } else {
  204. this._swapChildren(pTexture);
  205. this._destroyTexture();
  206. this.pseudoTexture = pTexture;
  207. this.texture = pTexture.texture;
  208. if (pTexture.shapes) this.pseudoTexture.shapes = this.addChild(pTexture.shapes);
  209. }
  210. if (this.pseudoTexture.html) this.addHTMLOverlay();
  211. if (refresh) this.refresh(configuration, { fullRefresh: !preview, refreshFilters });
  212. }
  213. refresh(configuration, { preview = false, fullRefresh = true, previewTexture = null, refreshFilters = false } = {}) {
  214. if (!this.overlayConfig || !this.texture) return;
  215. // Text preview handling
  216. if (previewTexture || this.originalTexture) {
  217. this.setTexture(previewTexture, { preview: Boolean(previewTexture), refresh: false });
  218. }
  219. // Register/Unregister hooks that should refresh this overlay
  220. if (configuration) {
  221. this._registerHooks(configuration);
  222. }
  223. const config = mergeObject(this.overlayConfig, configuration ?? {}, { inplace: !preview });
  224. if (preview && this.htmlOverlay) this.htmlOverlay.render(config, true);
  225. this.enableInteractivity(config);
  226. if (fullRefresh) {
  227. const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
  228. if (source && source.tagName === 'VIDEO') {
  229. if (!source.loop && config.loop) {
  230. game.video.play(source);
  231. } else if (source.loop && !config.loop) {
  232. game.video.stop(source);
  233. }
  234. source.loop = config.loop;
  235. }
  236. }
  237. const shapes = this.pseudoTexture.shapes;
  238. // Scale the image using the same logic as the token
  239. const dimensions = shapes ?? this.texture;
  240. if (config.linkScale && !config.parentID) {
  241. const scale = this.scale;
  242. const aspect = dimensions.width / dimensions.height;
  243. if (aspect >= 1) {
  244. scale.x = (this.object.w * this.object.document.texture.scaleX) / dimensions.width;
  245. scale.y = Number(scale.x);
  246. } else {
  247. scale.y = (this.object.h * this.object.document.texture.scaleY) / dimensions.height;
  248. scale.x = Number(scale.y);
  249. }
  250. } else if (config.linkStageScale) {
  251. this.scale.x = 1 / canvas.stage.scale.x;
  252. this.scale.y = 1 / canvas.stage.scale.y;
  253. } else if (config.linkDimensionsX || config.linkDimensionsY) {
  254. if (config.linkDimensionsX) {
  255. this.scale.x = this.object.document.width;
  256. }
  257. if (config.linkDimensionsY) {
  258. this.scale.y = this.object.document.height;
  259. }
  260. } else {
  261. this.scale.x = config.width ? config.width / dimensions.width : 1;
  262. this.scale.y = config.height ? config.height / dimensions.height : 1;
  263. }
  264. // Adjust scale according to config
  265. this.scale.x = this.scale.x * config.scaleX;
  266. this.scale.y = this.scale.y * config.scaleY;
  267. // Check if mirroring should be inherited from the token and if so apply it
  268. if (config.linkMirror && !config.parentID) {
  269. this.scale.x = Math.abs(this.scale.x) * (this.object.document.texture.scaleX < 0 ? -1 : 1);
  270. this.scale.y = Math.abs(this.scale.y) * (this.object.document.texture.scaleY < 0 ? -1 : 1);
  271. }
  272. if (this.anchor) {
  273. if (!config.anchor) this.anchor.set(0.5, 0.5);
  274. else this.anchor.set(config.anchor.x, config.anchor.y);
  275. }
  276. let xOff = 0;
  277. let yOff = 0;
  278. if (shapes) {
  279. shapes.position.x = -this.anchor.x * shapes.width;
  280. shapes.position.y = -this.anchor.y * shapes.height;
  281. if (config.animation.relative) {
  282. this.pivot.set(0, 0);
  283. shapes.pivot.set((0.5 - this.anchor.x) * shapes.width, (0.5 - this.anchor.y) * shapes.height);
  284. xOff = shapes.pivot.x * this.scale.x;
  285. yOff = shapes.pivot.y * this.scale.y;
  286. }
  287. } else if (config.animation.relative) {
  288. xOff = (0.5 - this.anchor.x) * this.width;
  289. yOff = (0.5 - this.anchor.y) * this.height;
  290. this.pivot.set((0.5 - this.anchor.x) * this.texture.width, (0.5 - this.anchor.y) * this.texture.height);
  291. }
  292. // Position
  293. const pOffsetX = config.pOffsetX || 0;
  294. const pOffsetY = config.pOffsetY || 0;
  295. if (config.parentID) {
  296. const anchor = this.parent.anchor ?? { x: 0, y: 0 };
  297. const pWidth = (this.parent.shapesWidth ?? this.parent.width) / this.parent.scale.x;
  298. const pHeight = (this.parent.shapesHeight ?? this.parent.height) / this.parent.scale.y;
  299. this.position.set(
  300. pOffsetX + -config.offsetX * pWidth - anchor.x * pWidth + pWidth / 2,
  301. pOffsetY + -config.offsetY * pHeight - anchor.y * pHeight + pHeight / 2
  302. );
  303. } else {
  304. if (config.animation.relative) {
  305. this.position.set(
  306. this.object.document.x + this.object.w / 2 + pOffsetX + -config.offsetX * this.object.w + xOff,
  307. this.object.document.y + this.object.h / 2 + pOffsetY + -config.offsetY * this.object.h + yOff
  308. );
  309. } else {
  310. this.position.set(this.object.document.x + this.object.w / 2, this.object.document.y + this.object.h / 2);
  311. this.pivot.set(
  312. -pOffsetX / this.scale.x + (config.offsetX * this.object.w) / this.scale.x,
  313. -pOffsetY / this.scale.y + (config.offsetY * this.object.h) / this.scale.y
  314. );
  315. }
  316. }
  317. // Set alpha but only if playOnce is disabled and the video hasn't
  318. // finished playing yet. Otherwise we want to keep alpha as 0 to keep the video hidden
  319. if (!this.tvaVideoEnded) {
  320. this.alpha = config.linkOpacity ? this.object.document.alpha : config.alpha;
  321. }
  322. // Angle in degrees
  323. if (fullRefresh) {
  324. if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
  325. else this.angle = config.angle;
  326. } else if (!config.animation.rotate) {
  327. if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
  328. }
  329. // Apply color tinting
  330. const tint = config.inheritTint
  331. ? this.object.document.texture.tint
  332. : interpolateColor(config.tint, config.interpolateColor, true);
  333. this.tint = tint ? Color.from(tint) : 0xffffff;
  334. if (shapes) {
  335. shapes.tint = this.tint;
  336. shapes.alpha = this.alpha;
  337. }
  338. if (fullRefresh) {
  339. if (config.animation.rotate) {
  340. this.animate(config);
  341. } else {
  342. this.stopAnimation();
  343. }
  344. }
  345. // Apply filters
  346. if (fullRefresh && (refreshFilters || !this.filtersApplied)) this._applyFilters(config);
  347. //if (fullRefresh) this.filters = this._getFilters(config);
  348. if (preview && this.children) {
  349. this.children.forEach((ch) => {
  350. if (ch instanceof TVAOverlay) ch.refresh(null, { preview: true });
  351. });
  352. }
  353. if (this.htmlOverlay) {
  354. this.htmlOverlay.setPosition({
  355. left: this.x - this.pivot.x * this.scale.x - this.width * this.anchor.x,
  356. top: this.y - this.pivot.y * this.scale.y - this.height * this.anchor.y,
  357. width: this.width,
  358. height: this.height,
  359. angle: this.angle,
  360. });
  361. }
  362. this.ready = true;
  363. }
  364. _activateTicker() {
  365. this._deactivateTicker();
  366. canvas.app.ticker.add(this.updatePosition, this, PIXI.UPDATE_PRIORITY.HIGH);
  367. }
  368. _deactivateTicker() {
  369. canvas.app.ticker.remove(this.updatePosition, this);
  370. }
  371. updatePosition() {
  372. let coord = canvas.canvasCoordinatesFromClient({
  373. x: window.innerWidth / 2 + this.overlayConfig.offsetX * window.innerWidth,
  374. y: window.innerHeight / 2 + this.overlayConfig.offsetY * window.innerHeight,
  375. });
  376. this.position.set(coord.x, coord.y);
  377. }
  378. async _applyFilters(config) {
  379. this.filtersApplied = true;
  380. const filterName = config.filter;
  381. const FilterClass = PIXI.filters[filterName];
  382. const options = mergeObject(FILTERS[filterName]?.defaultValues || {}, config.filterOptions);
  383. let filter;
  384. if (FilterClass) {
  385. if (FILTERS[filterName]?.argType === 'args') {
  386. let args = [];
  387. const controls = FILTERS[filterName]?.controls;
  388. if (controls) {
  389. controls.forEach((c) => args.push(options[c.name]));
  390. }
  391. filter = new FilterClass(...args);
  392. } else if (FILTERS[filterName]?.argType === 'options') {
  393. filter = new FilterClass(options);
  394. } else {
  395. filter = new FilterClass();
  396. }
  397. } else if (filterName === 'OutlineOverlayFilter') {
  398. filter = OutlineFilter.create(options);
  399. filter.thickness = options.trueThickness ?? 1;
  400. filter.animate = options.animate ?? false;
  401. } else if (filterName === 'Token Magic FX') {
  402. this.applyTVAFilters(await constructTMFXFilters(options.params || [], this));
  403. return;
  404. }
  405. if (filter) {
  406. this.applyTVAFilters([filter]);
  407. this.filters = [filter];
  408. } else {
  409. this.removeTVAFilters();
  410. }
  411. if (this.overlayConfig.ui && this.overlayConfig.bottom) this.applyReverseMask();
  412. else this.removeReverseMask();
  413. }
  414. applyReverseMask() {
  415. if (!this.filters?.find((f) => f.tvaReverse)) {
  416. const filters = this.filters || [];
  417. const reverseMask = ReverseMaskFilter.create({
  418. uMaskSampler: canvas.primary.tokensRenderTexture,
  419. channel: 'a',
  420. });
  421. reverseMask.tvaReverse = true;
  422. filters.push(reverseMask);
  423. this.filters = filters;
  424. }
  425. if (!this.filters) filters = [];
  426. }
  427. removeReverseMask() {
  428. if (this.filters?.length) {
  429. this.filters = this.filters.filter((f) => !f.tvaReverse);
  430. }
  431. }
  432. applyTVAFilters(filters) {
  433. if (filters?.length) {
  434. this.removeTVAFilters();
  435. this.filters = (this.filters || []).concat(filters);
  436. }
  437. }
  438. removeTVAFilters() {
  439. if (this.filters) this.filters = this.filters.filter((f) => !f.tvaFilter);
  440. }
  441. async stopAnimation() {
  442. if (this.animationName) {
  443. CanvasAnimation.terminateAnimation(this.animationName);
  444. }
  445. }
  446. async animate(config) {
  447. if (!this.animationName) this.animationName = this.object.sourceId + '.' + randomID(5);
  448. let newAngle = this.angle + (config.animation.clockwise ? 360 : -360);
  449. const rotate = [{ parent: this, attribute: 'angle', to: newAngle }];
  450. const completed = await CanvasAnimation.animate(rotate, {
  451. duration: config.animation.duration,
  452. name: this.animationName,
  453. });
  454. if (completed) {
  455. this.animate(config);
  456. }
  457. }
  458. _registerHooks(configuration) {
  459. if (configuration.linkStageScale) registerOverlayRefreshHook(this, 'canvasPan');
  460. else unregisterOverlayRefreshHooks(this, 'canvasPan');
  461. }
  462. _swapChildren(to) {
  463. const from = this.pseudoTexture;
  464. if (from.shapes) {
  465. this.removeChild(this.pseudoTexture.shapes);
  466. const children = from.shapes.removeChildren();
  467. if (to?.shapes) children.forEach((c) => to.shapes.addChild(c)?.refresh());
  468. else children.forEach((c) => this.addChild(c)?.refresh());
  469. } else if (to?.shapes) {
  470. const children = this.removeChildren();
  471. children.forEach((c) => to.shapes.addChild(c)?.refresh());
  472. }
  473. }
  474. _destroyTexture() {
  475. if (this.texture.textLabel || this.texture.destroyable) {
  476. this.texture.destroy(true);
  477. }
  478. if (this.pseudoTexture?.shapes) {
  479. this.removeChild(this.pseudoTexture.shapes);
  480. this.pseudoTexture.shapes.destroy();
  481. } else if (this.pseudoTexture?.html) {
  482. this.removeHTMLOverlay();
  483. }
  484. }
  485. destroy() {
  486. this.stopAnimation();
  487. unregisterOverlayRefreshHooks(this);
  488. if (this.children) {
  489. for (const ch of this.children) {
  490. if (ch instanceof TVAOverlay) ch.tvaRemove = true;
  491. }
  492. removeMarkedOverlays(this.object);
  493. if (this.pseudoTexture.shapes) {
  494. this.pseudoTexture.shapes.children.forEach((c) => c.destroy());
  495. this.removeChild(this.pseudoTexture.shapes)?.destroy();
  496. // this.pseudoTexture.shapes.destroy();
  497. }
  498. }
  499. if (this.texture.textLabel || this.texture.destroyable) {
  500. return super.destroy(true);
  501. } else if (this.texture?.baseTexture.resource?.source?.tagName === 'VIDEO') {
  502. this.texture.baseTexture.destroy();
  503. }
  504. this.removeHTMLOverlay();
  505. super.destroy();
  506. }
  507. // Foundry BUG Fix
  508. calculateTrimmedVertices() {
  509. return PIXI.Sprite.prototype.calculateTrimmedVertices.call(this);
  510. }
  511. addHTMLOverlay() {
  512. if (!this.htmlOverlay) this.htmlOverlay = new HTMLOverlay(this.overlayConfig, this.object);
  513. }
  514. removeHTMLOverlay() {
  515. if (this.htmlOverlay) this.htmlOverlay.remove();
  516. this.htmlOverlay = null;
  517. }
  518. }
  519. async function constructTMFXFilters(paramsArray, sprite) {
  520. if (typeof TokenMagic === 'undefined' || !('filterTypes' in TokenMagic)) return [];
  521. try {
  522. paramsArray = eval(paramsArray);
  523. } catch (e) {
  524. return [];
  525. }
  526. if (!Array.isArray(paramsArray)) {
  527. paramsArray = TokenMagic.getPreset(paramsArray);
  528. }
  529. if (!(paramsArray instanceof Array && paramsArray.length > 0)) return [];
  530. let filters = [];
  531. for (const params of paramsArray) {
  532. if (!params.hasOwnProperty('filterType') || !TokenMagic.filterTypes.hasOwnProperty(params.filterType)) {
  533. // one invalid ? all rejected.
  534. return [];
  535. }
  536. if (!params.hasOwnProperty('rank')) {
  537. params.rank = 5000;
  538. }
  539. if (!params.hasOwnProperty('filterId') || params.filterId == null) {
  540. params.filterId = randomID();
  541. }
  542. if (!params.hasOwnProperty('enabled') || !(typeof params.enabled === 'boolean')) {
  543. params.enabled = true;
  544. }
  545. params.filterInternalId = randomID();
  546. const gms = game.users.filter((user) => user.isGM);
  547. params.filterOwner = gms.length ? gms[0].id : game.data.userId;
  548. // params.placeableType = placeable._TMFXgetPlaceableType();
  549. params.updateId = randomID();
  550. const filterClass = TokenMagic.filterTypes[params.filterType];
  551. if (filterClass) {
  552. class ModTMFX extends filterClass {
  553. assignPlaceable() {
  554. this.targetPlaceable = sprite.object;
  555. this.placeableImg = sprite;
  556. }
  557. async _TMFXsetAnimeFlag() {}
  558. }
  559. const filter = new ModTMFX(params);
  560. if (filter) {
  561. // Patch fixes
  562. filter.placeableImg = sprite;
  563. filter.targetPlaceable = sprite.object;
  564. // end of fixes
  565. filter.tvaFilter = true;
  566. filters.unshift(filter);
  567. }
  568. }
  569. }
  570. return filters;
  571. }
  572. class OutlineFilter extends OutlineOverlayFilter {
  573. /** @inheritdoc */
  574. static createFragmentShader() {
  575. return `
  576. varying vec2 vTextureCoord;
  577. varying vec2 vFilterCoord;
  578. uniform sampler2D uSampler;
  579. uniform vec2 thickness;
  580. uniform vec4 outlineColor;
  581. uniform vec4 filterClamp;
  582. uniform float alphaThreshold;
  583. uniform float time;
  584. uniform bool knockout;
  585. uniform bool wave;
  586. ${this.CONSTANTS}
  587. ${this.WAVE()}
  588. void main(void) {
  589. float dist = distance(vFilterCoord, vec2(0.5)) * 2.0;
  590. vec4 ownColor = texture2D(uSampler, vTextureCoord);
  591. vec4 wColor = wave ? outlineColor *
  592. wcos(0.0, 1.0, dist * 75.0,
  593. -time * 0.01 + 3.0 * dot(vec4(1.0), ownColor))
  594. * 0.33 * (1.0 - dist) : vec4(0.0);
  595. float texAlpha = smoothstep(alphaThreshold, 1.0, ownColor.a);
  596. vec4 curColor;
  597. float maxAlpha = 0.;
  598. vec2 displaced;
  599. for ( float angle = 0.0; angle <= TWOPI; angle += ${this.#quality.toFixed(7)} ) {
  600. displaced.x = vTextureCoord.x + thickness.x * cos(angle);
  601. displaced.y = vTextureCoord.y + thickness.y * sin(angle);
  602. curColor = texture2D(uSampler, clamp(displaced, filterClamp.xy, filterClamp.zw));
  603. curColor.a = clamp((curColor.a - 0.6) * 2.5, 0.0, 1.0);
  604. maxAlpha = max(maxAlpha, curColor.a);
  605. }
  606. float resultAlpha = max(maxAlpha, texAlpha);
  607. vec3 result = (ownColor.rgb + outlineColor.rgb * (1.0 - texAlpha)) * resultAlpha;
  608. gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha);
  609. }
  610. `;
  611. }
  612. static get #quality() {
  613. switch (canvas.performance.mode) {
  614. case CONST.CANVAS_PERFORMANCE_MODES.LOW:
  615. return (Math.PI * 2) / 10;
  616. case CONST.CANVAS_PERFORMANCE_MODES.MED:
  617. return (Math.PI * 2) / 20;
  618. default:
  619. return (Math.PI * 2) / 30;
  620. }
  621. }
  622. }