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.

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