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.

359 lines
12 KiB

  1. import EffectMappingForm from '../applications/effectMappingForm.js';
  2. import { OverlayConfig } from '../applications/overlayConfig.js';
  3. import { TVAOverlay } from './sprite/TVAOverlay.js';
  4. import { evaluateOverlayExpressions } from './token/overlay.js';
  5. export class Reticle {
  6. static app;
  7. static fields;
  8. static reticleOverlay;
  9. static active = false;
  10. static hitTest;
  11. static token = null;
  12. static dialog = null;
  13. // Offset calculation controls
  14. static mode = 'tooltip';
  15. static increment = 1;
  16. static _onReticleMove(event) {
  17. if (this.reticleOverlay.isMouseDown) {
  18. let pos = event.data.getLocalPosition(this.reticleOverlay);
  19. this.config.pOffsetX = 0;
  20. this.config.pOffsetY = 0;
  21. this.config.offsetX = 0;
  22. this.config.offsetY = 0;
  23. if (this.mode === 'token') {
  24. this.config.linkRotation = true;
  25. this.config.linkMirror = true;
  26. }
  27. this.tvaOverlay.refresh(this.config, { preview: true });
  28. const tCoord = { x: this.tvaOverlay.x, y: this.tvaOverlay.y };
  29. if (this.tvaOverlay.overlayConfig.parentID) {
  30. let parent = this.tvaOverlay;
  31. do {
  32. parent = parent.parent;
  33. tCoord.x += parent.x;
  34. tCoord.y += parent.y;
  35. } while (!(parent instanceof TVAOverlay));
  36. }
  37. let dx = pos.x - tCoord.x;
  38. let dy = pos.y - tCoord.y;
  39. let angle = 0;
  40. if (!this.config.animation.relative) {
  41. angle = this.config.angle;
  42. if (this.config.linkRotation) angle += this.tvaOverlay.object.document.rotation;
  43. }
  44. [dx, dy] = rotate(0, 0, dx, dy, angle);
  45. dx = round(dx, this.increment);
  46. dy = round(dy, this.increment);
  47. // let lPos = event.data.getLocalPosition(this.tvaOverlay);
  48. // console.log(lPos);
  49. // // let dx = lPos.x;
  50. // // let dy = lPos.y;
  51. if (this.mode === 'static') {
  52. this.config.pOffsetX = dx;
  53. this.config.pOffsetY = dy;
  54. } else if (this.mode === 'token') {
  55. this.config.offsetX = -dx / this.tvaOverlay.object.w;
  56. this.config.offsetY = -dy / this.tvaOverlay.object.h;
  57. } else {
  58. let token = this.tvaOverlay.object;
  59. let pWidth;
  60. let pHeight;
  61. if (this.tvaOverlay.overlayConfig.parentID) {
  62. pWidth =
  63. (this.tvaOverlay.parent.shapesWidth ?? this.tvaOverlay.parent.width) / this.tvaOverlay.parent.scale.x;
  64. pHeight =
  65. (this.tvaOverlay.parent.shapesHeight ?? this.tvaOverlay.parent.height) / this.tvaOverlay.parent.scale.y;
  66. } else {
  67. pWidth = token.w;
  68. pHeight = token.h;
  69. }
  70. if (this.mode === 'tooltip') {
  71. if (Math.abs(dx) >= pWidth / 2) {
  72. this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
  73. dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
  74. } else {
  75. this.config.offsetX = -dx / this.tvaOverlay.object.w;
  76. dx = 0;
  77. }
  78. if (Math.abs(dy) >= pHeight / 2) {
  79. this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
  80. dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
  81. } else {
  82. this.config.offsetY = -dy / this.tvaOverlay.object.h;
  83. dy = 0;
  84. }
  85. } else {
  86. if (Math.abs(dx) >= pWidth / 2) {
  87. this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
  88. dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
  89. } else if (Math.abs(dy) >= pHeight / 2) {
  90. this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
  91. dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
  92. } else {
  93. this.config.offsetX = -dx / this.tvaOverlay.object.w;
  94. dx = 0;
  95. this.config.offsetY = -dy / this.tvaOverlay.object.h;
  96. dy = 0;
  97. }
  98. }
  99. this.config.pOffsetX = dx;
  100. this.config.pOffsetY = dy;
  101. }
  102. this.tvaOverlay.refresh(this.config, { preview: true });
  103. }
  104. }
  105. static minimizeApps() {
  106. Object.values(ui.windows).forEach((app) => {
  107. if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
  108. app.minimize();
  109. }
  110. });
  111. }
  112. static maximizeApps() {
  113. Object.values(ui.windows).forEach((app) => {
  114. if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
  115. app.maximize();
  116. }
  117. });
  118. }
  119. static activate({ tvaOverlay = null, config = {} } = {}) {
  120. if (this.deactivate() || !canvas.ready) return false;
  121. if (!tvaOverlay || !config) return false;
  122. if (this.reticleOverlay) {
  123. this.reticleOverlay.destroy(true);
  124. }
  125. const interaction = canvas.app.renderer.plugins.interaction;
  126. if (!interaction.cursorStyles['reticle']) {
  127. interaction.cursorStyles['reticle'] = "url('modules/token-variants/img/reticle.webp'), auto";
  128. }
  129. this.tvaOverlay = tvaOverlay;
  130. this.minimizeApps();
  131. this.config = evaluateOverlayExpressions(deepClone(config), this.tvaOverlay.object, {
  132. overlayConfig: config,
  133. });
  134. // Setup the overlay to be always visible while we're adjusting its position
  135. this.config.alwaysVisible = true;
  136. this.active = true;
  137. // Create the reticle overlay
  138. this.reticleOverlay = new PIXI.Container();
  139. this.reticleOverlay.hitArea = canvas.dimensions.rect;
  140. this.reticleOverlay.cursor = 'reticle';
  141. this.reticleOverlay.interactive = true;
  142. this.reticleOverlay.zIndex = Infinity;
  143. const stopEvent = function (event) {
  144. event.preventDefault();
  145. // event.stopPropagation();
  146. };
  147. this.reticleOverlay.on('mousedown', (event) => {
  148. event.preventDefault();
  149. if (event.data.originalEvent.which != 2 && event.data.originalEvent.nativeEvent.which != 2) {
  150. this.reticleOverlay.isMouseDown = true;
  151. this._onReticleMove(event);
  152. }
  153. });
  154. this.reticleOverlay.on('pointermove', (event) => {
  155. event.preventDefault();
  156. // event.stopPropagation();
  157. this._onReticleMove(event);
  158. });
  159. this.reticleOverlay.on('mouseup', (event) => {
  160. event.preventDefault();
  161. this.reticleOverlay.isMouseDown = false;
  162. });
  163. this.reticleOverlay.on('click', (event) => {
  164. event.preventDefault();
  165. if (event.data.originalEvent.which == 2 || event.data.originalEvent.nativeEvent.which == 2) {
  166. this.deactivate();
  167. }
  168. });
  169. canvas.stage.addChild(this.reticleOverlay);
  170. this.dialog = displayControlDialog();
  171. return true;
  172. }
  173. static deactivate() {
  174. if (this.active) {
  175. if (this.reticleOverlay) this.reticleOverlay.parent?.removeChild(this.reticleOverlay);
  176. this.active = false;
  177. this.tvaOverlay = null;
  178. if (this.dialog && this.dialog._state !== Application.RENDER_STATES.CLOSED) this.dialog.close(true);
  179. this.dialog = null;
  180. this.maximizeApps();
  181. const app = Object.values(ui.windows).find((app) => app instanceof OverlayConfig);
  182. if (!app) {
  183. this.config = null;
  184. return;
  185. }
  186. const form = $(app.form);
  187. ['pOffsetX', 'pOffsetY', 'offsetX', 'offsetY'].forEach((field) => {
  188. if (field in this.config) {
  189. form.find(`[name="${field}"]`).val(this.config[field]);
  190. }
  191. });
  192. if (this.mode === 'token') {
  193. ['linkRotation', 'linkMirror'].forEach((field) => {
  194. form.find(`[name="${field}"]`).prop('checked', true);
  195. });
  196. ['linkDimensionsX', 'linkDimensionsY'].forEach((field) => {
  197. form.find(`[name="${field}"]`).prop('checked', false);
  198. });
  199. } else {
  200. ['linkRotation', 'linkMirror'].forEach((field) => {
  201. form.find(`[name="${field}"]`).prop('checked', false);
  202. });
  203. }
  204. if (this.mode === 'hud') {
  205. form.find('[name="ui"]').prop('checked', true).trigger('change');
  206. }
  207. form.find('[name="anchor.x"]').val(this.config.anchor.x);
  208. form.find('[name="anchor.y"]').val(this.config.anchor.y).trigger('change');
  209. this.config = null;
  210. return true;
  211. }
  212. }
  213. }
  214. function displayControlDialog() {
  215. const d = new Dialog({
  216. title: 'Set Overlay Position',
  217. content: `
  218. <style>
  219. .images { display: flex; }
  220. .images a { flex: 20%; width: 50px; margin: 2px; }
  221. .images a.active img { border-color: orange; border-width: 2px; }
  222. .anchorlbl {margin: auto; display: table; }
  223. </style>
  224. <div class="images">
  225. <a data-id="token"><img src="modules/token-variants/img/token_mode.png"></img></a>
  226. <a data-id="tooltip"><img src="modules/token-variants/img/tooltip_mode.png"></img></a>
  227. <a data-id="hud"><img src="modules/token-variants/img/hud_mode.png"></img></a>
  228. <a data-id="static"><img src="modules/token-variants/img/static_mode.png"></img></a>
  229. </div>
  230. <br>
  231. <label class="anchorlbl">Anchor</label>
  232. <div class="tva-anchor">
  233. <input type="radio" class="top left" name="anchor">
  234. <input type="radio" class="top center" name="anchor">
  235. <input type="radio" class="top right" name="anchor">
  236. <input type="radio" class="mid left" name="anchor">
  237. <input type="radio" class="mid center" name="anchor">
  238. <input type="radio" class="mid right" name="anchor">
  239. <input type="radio" class="bot left" name="anchor">
  240. <input type="radio" class="bot center" name="anchor">
  241. <input type="radio" class="bot right" name="anchor">
  242. </div>
  243. <div class="form-group">
  244. <label>Step Size</label>
  245. <div class="form-fields">
  246. <input type="number" name="step" min="0" step="1" value="${Reticle.increment}">
  247. </div>
  248. </div>
  249. <p class="notes"><b>Left-Click</b> to move the overlay</p>
  250. <p class="notes"><b>Middle-Click</b> or <b>Close Dialog</b> to exit overlay positioning</p>
  251. `,
  252. buttons: {},
  253. render: (html) => {
  254. // Mode Images
  255. const images = html.find('.images a');
  256. html.find('.images a').on('click', (event) => {
  257. images.removeClass('active');
  258. const target = $(event.target).closest('a');
  259. target.addClass('active');
  260. Reticle.mode = target.data('id');
  261. });
  262. html.find(`[data-id="${Reticle.mode}"]`).addClass('active');
  263. // Anchor
  264. let anchorX = Reticle.config?.anchor?.x || 0;
  265. let anchorY = Reticle.config?.anchor?.y || 0;
  266. let classes = '';
  267. if (anchorX < 0.5) classes += '.left';
  268. else if (anchorX > 0.5) classes += '.right';
  269. else classes += '.center';
  270. if (anchorY < 0.5) classes += '.top';
  271. else if (anchorY > 0.5) classes += '.bot';
  272. else classes += '.mid';
  273. html.find('.tva-anchor').find(classes).prop('checked', true);
  274. // end - Pre-select anchor
  275. html.find('input[name="anchor"]').on('change', (event) => {
  276. const anchor = $(event.target);
  277. let x;
  278. let y;
  279. if (anchor.hasClass('left')) x = 0;
  280. else if (anchor.hasClass('center')) x = 0.5;
  281. else x = 1;
  282. if (anchor.hasClass('top')) y = 0;
  283. else if (anchor.hasClass('mid')) y = 0.5;
  284. else y = 1;
  285. Reticle.config.anchor.x = x;
  286. Reticle.config.anchor.y = y;
  287. });
  288. html.find('[name="step"]').on('input', (event) => {
  289. Reticle.increment = $(event.target).val() || 1;
  290. });
  291. },
  292. close: () => Reticle.deactivate(),
  293. });
  294. d.render(true);
  295. setTimeout(() => d.setPosition({ left: 200, top: window.innerHeight / 2, height: 'auto' }), 100);
  296. return d;
  297. }
  298. function round(number, increment, offset = 0) {
  299. return Math.ceil((number - offset) / increment) * increment + offset;
  300. }
  301. function rotate(cx, cy, x, y, angle) {
  302. var radians = (Math.PI / 180) * angle,
  303. cos = Math.cos(radians),
  304. sin = Math.sin(radians),
  305. nx = cos * (x - cx) + sin * (y - cy) + cx,
  306. ny = cos * (y - cy) - sin * (x - cx) + cy;
  307. return [nx, ny];
  308. }