Keybindings for Inkarnate

Adds keybindings to all mapping function in https://inkarnate.com/

  1. // ==UserScript==
  2. // @name Keybindings for Inkarnate
  3. // @description Adds keybindings to all mapping function in https://inkarnate.com/
  4. // @namespace azzurite
  5. // @version 1.2.0
  6. // @locale en
  7. // @author azzurite
  8. // @match https://inkarnate.com/maps
  9. // @run-at document-idle
  10. // @noframes
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const keybinds = {
  19. sculpt: {
  20. mode: {
  21. add: 'a',
  22. subtract: 's'
  23. },
  24. shape: {
  25. circle: 'c',
  26. block: 'b',
  27. hex: 'h'
  28. },
  29. size: {
  30. more: 'ArrowRight',
  31. less: 'ArrowLeft',
  32. moreMore: 'ArrowUp',
  33. lessLess: 'ArrowDown'
  34. }
  35. },
  36. brush: {
  37. chooseTexture: {
  38. open: 'q'
  39. },
  40. layer: {
  41. fg: 'f',
  42. bg: 'g'
  43. },
  44. shape: {
  45. circle: 'c',
  46. block: 'b',
  47. hex: 'h'
  48. },
  49. size: {
  50. more: 'ArrowRight',
  51. less: 'ArrowLeft',
  52. moreMore: 'ArrowUp',
  53. lessLess: 'ArrowDown'
  54. },
  55. softness: {
  56. more: 'o',
  57. less: 'i',
  58. moreMore: 'p',
  59. lessLess: 'u'
  60. },
  61. opacity: {
  62. moreMore: 'l',
  63. lessLess: 'k'
  64. }
  65. },
  66. object: {
  67. mode: {
  68. place: 'q',
  69. select: 'w'
  70. },
  71. layer: {
  72. fg: 'f',
  73. bg: 'g'
  74. },
  75. chooseObject: {
  76. open: 'e'
  77. },
  78. scale: {
  79. value: {
  80. more: 'o',
  81. less: 'i',
  82. moreMore: 'p',
  83. lessLess: 'u'
  84. }
  85. },
  86. },
  87. pattern: {
  88. mode: {
  89. place: 'q',
  90. select: 'w'
  91. },
  92. choosePattern: {
  93. open: 'e'
  94. },
  95. scale: {
  96. more: 'o',
  97. less: 'i',
  98. moreMore: 'p',
  99. lessLess: 'u'
  100. },
  101. cycleSelection: {
  102. cycle: 'c'
  103. }
  104. },
  105. text: {
  106. mode: {
  107. place: 'q',
  108. select: 'w'
  109. },
  110. size: {
  111. more: 'o',
  112. less: 'i',
  113. moreMore: 'p',
  114. lessLess: 'u'
  115. },
  116. bold: {
  117. toggle: 'b'
  118. }
  119. },
  120. note: {
  121. mode: {
  122. place: 'q',
  123. select: 'w'
  124. },
  125. },
  126. grid: {
  127. type: {
  128. hex: 'h',
  129. square: 's'
  130. },
  131. opacity: {
  132. more: 'o',
  133. less: 'i',
  134. moreMore: 'p',
  135. lessLess: 'u'
  136. },
  137. size: {
  138. more: 'ArrowRight',
  139. less: 'ArrowLeft',
  140. moreMore: 'ArrowUp',
  141. lessLess: 'ArrowDown'
  142. },
  143. width: {
  144. more: 'l',
  145. less: 'k'
  146. }
  147. },
  148. global: {
  149. tools: {
  150. sculpt: '1',
  151. brush: '2',
  152. object: '3',
  153. pattern: '4',
  154. text: '5',
  155. note: '6',
  156. grid: '7',
  157. zoom: '8'
  158. },
  159. zoom: {
  160. upleft: 103, // these are key codes for the numpad
  161. up: 104,
  162. upright: 105,
  163. right: 102,
  164. downright: 99,
  165. down: 98,
  166. downleft: 97,
  167. left: 100,
  168. zoomIn: 107,
  169. zoomOut: 109,
  170. reset: 101
  171. },
  172. delete: 'Delete'
  173. }
  174. };
  175.  
  176. // from https://stackoverflow.com/a/53739792/1805439
  177. function flattenObject(ob) {
  178. var toReturn = {};
  179.  
  180. for (var i in ob) {
  181. if (!ob.hasOwnProperty(i)) continue;
  182.  
  183. if ((typeof ob[i]) == 'object' && ob[i] !== null) {
  184. var flatObject = flattenObject(ob[i]);
  185. for (var x in flatObject) {
  186. if (!flatObject.hasOwnProperty(x)) continue;
  187.  
  188. toReturn[i + '.' + x] = flatObject[x];
  189. }
  190. } else {
  191. toReturn[i] = ob[i];
  192. }
  193. }
  194. return toReturn;
  195. }
  196.  
  197. // inkarnate has jquery + angular
  198. let $ = window.jQuery;
  199. let angular = window.angular;
  200.  
  201. const defaultSteps = {
  202. more: 1,
  203. less: -1,
  204. moreMore: 5,
  205. lessLess: -5
  206. };
  207. let configs = {
  208. sculpt: {
  209. size: {
  210. type: 'number',
  211. min: 3,
  212. max: 128
  213. }
  214. },
  215. brush: {
  216. chooseTexture: {
  217. type: 'button',
  218. selector: '#brush-texture-popup'
  219. },
  220. size: {
  221. type: 'number',
  222. min: 1,
  223. max: 128
  224. },
  225. softness: {
  226. type: 'number',
  227. min: 0,
  228. max: 1,
  229. more: 0.01,
  230. less: -0.01,
  231. moreMore: 0.05,
  232. lessLess: -0.05,
  233. decimalPlaces: 2
  234. },
  235. opacity: {
  236. type: 'number',
  237. min: 0,
  238. max: 1,
  239. more: 0.01,
  240. less: -0.01,
  241. moreMore: 0.05,
  242. lessLess: -0.05,
  243. decimalPlaces: 2
  244. }
  245. },
  246. object: {
  247. chooseObject: {
  248. type: 'button',
  249. selector: '#object-popup'
  250. },
  251. scale: {
  252. value: {
  253. type: 'number',
  254. min: 0.15,
  255. max: 2,
  256. more: 0.01,
  257. less: -0.01,
  258. moreMore: 0.05,
  259. lessLess: -0.05,
  260. decimalPlaces: 2
  261. }
  262. },
  263. },
  264. pattern: {
  265. choosePattern: {
  266. type: 'button',
  267. selector: '#pattern-popup'
  268. },
  269. scale: {
  270. type: 'number',
  271. alternateProperty: 'object.globalScale',
  272. min: 0.15,
  273. max: 2,
  274. more: 0.01,
  275. less: -0.01,
  276. moreMore: 0.05,
  277. lessLess: -0.05,
  278. decimalPlaces: 2
  279. },
  280. cycleSelection: {
  281. type: 'cycleSelection'
  282. }
  283. },
  284. text: {
  285. size: {
  286. type: 'number',
  287. min: 5,
  288. max: 128
  289. },
  290. bold: {
  291. type: 'toggle'
  292. },
  293. color: {
  294. type: 'button',
  295. selector: 'button.color-picker'
  296. },
  297. shadow: {
  298. type: 'button',
  299. selector: '#text-shadow-popup'
  300. }
  301. },
  302. grid: {
  303. opacity: {
  304. type: 'number',
  305. min: 0,
  306. max: 1,
  307. more: 0.01,
  308. less: -0.01,
  309. moreMore: 0.05,
  310. lessLess: -0.05,
  311. decimalPlaces: 2
  312. },
  313. size: {
  314. type: 'number',
  315. min: 16,
  316. max: 256
  317. },
  318. width: {
  319. type: 'number',
  320. min: 1,
  321. max: 5,
  322. more: 0.5,
  323. less: -0.5
  324. }
  325. },
  326. }
  327.  
  328. function getScope() {
  329. return angular.element(document.getElementById('map-builder-view')).scope();
  330. }
  331.  
  332. function getByPath(obj, path, separator = '.') {
  333. var parts = path.split('.');
  334. return parts.reduce((prev, curr) => prev && prev[curr], obj)
  335. }
  336.  
  337. function setByPath(obj, path, value) {
  338. var parts = path.split('.');
  339. parts.reduce(function(prev, cur, idx) {
  340. if (idx == parts.length - 1) {
  341. prev[cur] = value;
  342. } else {
  343. return prev[cur] || {};
  344. }
  345. }, obj);
  346. }
  347.  
  348. function changeScope(func) {
  349. let scope = getScope();
  350. scope.$apply(() => {
  351. func(scope);
  352. });
  353. }
  354.  
  355. function getCurrentTool() {
  356. return getScope().tool;
  357. }
  358.  
  359. function clampNumber(config, num) {
  360. const min = config.min || 0;
  361. const max = config.max || 128;
  362. return Math.min(Math.max(min, num), max);
  363. }
  364.  
  365. function roundNumber(config, num) {
  366. const decimalPlaces = config.decimalPlaces || 1;
  367. const multiplier = Math.pow(10, decimalPlaces);
  368. return Math.round(num * multiplier) / multiplier;
  369. }
  370.  
  371. function getStep(config, stepName) {
  372. return config[stepName] || defaultSteps[stepName];
  373. }
  374.  
  375. let originalSelection = [];
  376. let curIdx = -1;
  377. function cycleSelection() {
  378. let scopeSelection = getScope().selected;
  379.  
  380. if (scopeSelection.length <= 1 && curIdx === -1) {
  381. return;
  382. }
  383.  
  384. if (curIdx === -1) {
  385. curIdx = 0;
  386. originalSelection = scopeSelection;
  387. changeScope(s => {
  388. s.selected = [originalSelection[curIdx]];
  389. });
  390. return;
  391. }
  392.  
  393. if (originalSelection[curIdx] === scopeSelection[0]) {
  394. if (++curIdx >= originalSelection.length) {
  395. curIdx = -1;
  396. changeScope(s => {
  397. s.selected = originalSelection;
  398. });
  399. } else {
  400. changeScope(s => {
  401. s.selected = [originalSelection[curIdx]];
  402. });
  403. }
  404. } else {
  405. // user selected something different
  406. curIdx = -1;
  407. originalSelection = [];
  408. cycleSelection();
  409. }
  410. }
  411.  
  412. function handleKeyForTool(tool, key) {
  413. const binds = keybinds[tool];
  414.  
  415. const flatBinds = flattenObject(binds);
  416. for (const [path, bind] of Object.entries(flatBinds)) {
  417. if (key !== bind) {
  418. continue;
  419. }
  420.  
  421. const pathArr = path.split('.');
  422. let propPath = tool + '.' + pathArr.slice(0, pathArr.length - 1).join('.');
  423. const val = pathArr[pathArr.length - 1];
  424.  
  425. //console.log(`Found bind: ${propPath} with value ${val}`);
  426.  
  427. const config = getByPath(configs, propPath) || {};
  428. if (config.alternateProperty) {
  429. propPath = config.alternateProperty;
  430. }
  431. switch (config.type) {
  432. case 'cycleSelection': {
  433. cycleSelection();
  434. break;
  435. }
  436. case 'toggle': {
  437. changeScope(scope => {
  438. const oldVal = getByPath(scope, propPath);
  439. setByPath(scope, propPath, !oldVal);
  440. });
  441. break;
  442. }
  443. case 'button': {
  444. //console.log(`Clicking button ${config.selector}`);
  445. $(config.selector).click();
  446. break;
  447. }
  448. case 'number': {
  449. changeScope(scope => {
  450. const oldVal = getByPath(scope, propPath);
  451. const step = getStep(config, val);
  452. const newVal = oldVal + step;
  453. const newValClamped = clampNumber(config, newVal);
  454. const rounded = roundNumber(config, newValClamped);
  455. //console.log(`Set ${propPath} to ${rounded}`);
  456. setByPath(scope, propPath, rounded);
  457. });
  458. break;
  459. }
  460. default:
  461. case 'select': {
  462. changeScope(scope => {
  463. setByPath(scope, propPath, val);
  464. });
  465. break;
  466. }
  467. }
  468. }
  469. }
  470.  
  471. function handleZoom(action, altKey) {
  472. function move(direction, amount) {
  473. changeScope(s => { s.zoom.translate[direction] += amount * (altKey ? 3 : 1); });
  474. }
  475. function zoom(amount) {
  476. changeScope(s => {
  477. const newScale = s.zoom.scale * amount;
  478. s.zoom.scale = clampNumber({min: 1, max: 5}, roundNumber({decimalPlaces: 2}, newScale));
  479. });
  480. }
  481.  
  482. const actions = {
  483. upleft: () => { move('y', 10); move('x', 10); },
  484. up: move.bind(null, 'y', 10),
  485. upright: () => { move('y', 10); move('x', -10); },
  486. right: move.bind(null, 'x', -10),
  487. downright: () => { move('y', -10); move('x', -10); },
  488. down: move.bind(null, 'y', -10),
  489. downleft: () => { move('y', -10); move('x', 10); },
  490. left: move.bind(null, 'x', 10),
  491. zoomIn: zoom.bind(null, 1.05),
  492. zoomOut: zoom.bind(null, 0.95),
  493. reset: () => {
  494. changeScope(s => {
  495. s.zoom.scale = 1;
  496. s.zoom.translate.x = 0;
  497. s.zoom.translate.y = 0;
  498. });
  499. }
  500. };
  501.  
  502. actions[action]();
  503. }
  504.  
  505. function keyHandler(e) {
  506. if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
  507. return;
  508. }
  509.  
  510. if (keybinds.global.delete === e.key) {
  511. changeScope(s => {
  512. if (s.radialMenuActions().includes('delete')) {
  513. s.performRadialMenuAction('delete');
  514. }
  515. });
  516. return
  517. }
  518.  
  519. const isZoomKey = Object.values(keybinds.global.zoom).includes(e.keyCode);
  520. if (isZoomKey) {
  521. const [zoomAction, _] = Object.entries(keybinds.global.zoom).find(elem => elem[1] === e.keyCode);
  522. handleZoom(zoomAction, e.altKey);
  523. return;
  524. }
  525. const isToolKey = Object.values(keybinds.global.tools).includes(e.key);
  526. if (isToolKey) {
  527. const [tool, _] = Object.entries(keybinds.global.tools).find(elem => elem[1] === e.key);
  528. changeScope(scope => scope.selectTool(tool));
  529. return;
  530. }
  531.  
  532. // console.log(`Keypress: ${e.key}`);
  533. let cur = getCurrentTool();
  534. // console.log(`Cur tool: ${cur}`);
  535. if (!keybinds[cur]) {
  536. // console.log(`No Keybinds for tool "${getCurrentTool()}" configured.`);
  537. return;
  538. }
  539.  
  540. handleKeyForTool(cur, e.key);
  541. }
  542.  
  543.  
  544. $(document).keydown(keyHandler);
  545. })();