- // ==UserScript==
- // @name Keybindings for Inkarnate
- // @description Adds keybindings to all mapping function in https://inkarnate.com/
- // @namespace azzurite
- // @version 1.2.0
- // @locale en
- // @author azzurite
- // @match https://inkarnate.com/maps
- // @run-at document-idle
- // @noframes
- // @grant none
- // ==/UserScript==
-
-
- (function() {
- 'use strict';
-
- const keybinds = {
- sculpt: {
- mode: {
- add: 'a',
- subtract: 's'
- },
- shape: {
- circle: 'c',
- block: 'b',
- hex: 'h'
- },
- size: {
- more: 'ArrowRight',
- less: 'ArrowLeft',
- moreMore: 'ArrowUp',
- lessLess: 'ArrowDown'
- }
- },
- brush: {
- chooseTexture: {
- open: 'q'
- },
- layer: {
- fg: 'f',
- bg: 'g'
- },
- shape: {
- circle: 'c',
- block: 'b',
- hex: 'h'
- },
- size: {
- more: 'ArrowRight',
- less: 'ArrowLeft',
- moreMore: 'ArrowUp',
- lessLess: 'ArrowDown'
- },
- softness: {
- more: 'o',
- less: 'i',
- moreMore: 'p',
- lessLess: 'u'
- },
- opacity: {
- moreMore: 'l',
- lessLess: 'k'
- }
- },
- object: {
- mode: {
- place: 'q',
- select: 'w'
- },
- layer: {
- fg: 'f',
- bg: 'g'
- },
- chooseObject: {
- open: 'e'
- },
- scale: {
- value: {
- more: 'o',
- less: 'i',
- moreMore: 'p',
- lessLess: 'u'
- }
- },
- },
- pattern: {
- mode: {
- place: 'q',
- select: 'w'
- },
- choosePattern: {
- open: 'e'
- },
- scale: {
- more: 'o',
- less: 'i',
- moreMore: 'p',
- lessLess: 'u'
- },
- cycleSelection: {
- cycle: 'c'
- }
- },
- text: {
- mode: {
- place: 'q',
- select: 'w'
- },
- size: {
- more: 'o',
- less: 'i',
- moreMore: 'p',
- lessLess: 'u'
- },
- bold: {
- toggle: 'b'
- }
- },
- note: {
- mode: {
- place: 'q',
- select: 'w'
- },
- },
- grid: {
- type: {
- hex: 'h',
- square: 's'
- },
- opacity: {
- more: 'o',
- less: 'i',
- moreMore: 'p',
- lessLess: 'u'
- },
- size: {
- more: 'ArrowRight',
- less: 'ArrowLeft',
- moreMore: 'ArrowUp',
- lessLess: 'ArrowDown'
- },
- width: {
- more: 'l',
- less: 'k'
- }
- },
- global: {
- tools: {
- sculpt: '1',
- brush: '2',
- object: '3',
- pattern: '4',
- text: '5',
- note: '6',
- grid: '7',
- zoom: '8'
- },
- zoom: {
- upleft: 103, // these are key codes for the numpad
- up: 104,
- upright: 105,
- right: 102,
- downright: 99,
- down: 98,
- downleft: 97,
- left: 100,
- zoomIn: 107,
- zoomOut: 109,
- reset: 101
- },
- delete: 'Delete'
- }
- };
-
- // from https://stackoverflow.com/a/53739792/1805439
- function flattenObject(ob) {
- var toReturn = {};
-
- for (var i in ob) {
- if (!ob.hasOwnProperty(i)) continue;
-
- if ((typeof ob[i]) == 'object' && ob[i] !== null) {
- var flatObject = flattenObject(ob[i]);
- for (var x in flatObject) {
- if (!flatObject.hasOwnProperty(x)) continue;
-
- toReturn[i + '.' + x] = flatObject[x];
- }
- } else {
- toReturn[i] = ob[i];
- }
- }
- return toReturn;
- }
-
- // inkarnate has jquery + angular
- let $ = window.jQuery;
- let angular = window.angular;
-
- const defaultSteps = {
- more: 1,
- less: -1,
- moreMore: 5,
- lessLess: -5
- };
- let configs = {
- sculpt: {
- size: {
- type: 'number',
- min: 3,
- max: 128
- }
- },
- brush: {
- chooseTexture: {
- type: 'button',
- selector: '#brush-texture-popup'
- },
- size: {
- type: 'number',
- min: 1,
- max: 128
- },
- softness: {
- type: 'number',
- min: 0,
- max: 1,
- more: 0.01,
- less: -0.01,
- moreMore: 0.05,
- lessLess: -0.05,
- decimalPlaces: 2
- },
- opacity: {
- type: 'number',
- min: 0,
- max: 1,
- more: 0.01,
- less: -0.01,
- moreMore: 0.05,
- lessLess: -0.05,
- decimalPlaces: 2
- }
- },
- object: {
- chooseObject: {
- type: 'button',
- selector: '#object-popup'
- },
- scale: {
- value: {
- type: 'number',
- min: 0.15,
- max: 2,
- more: 0.01,
- less: -0.01,
- moreMore: 0.05,
- lessLess: -0.05,
- decimalPlaces: 2
- }
- },
- },
- pattern: {
- choosePattern: {
- type: 'button',
- selector: '#pattern-popup'
- },
- scale: {
- type: 'number',
- alternateProperty: 'object.globalScale',
- min: 0.15,
- max: 2,
- more: 0.01,
- less: -0.01,
- moreMore: 0.05,
- lessLess: -0.05,
- decimalPlaces: 2
- },
- cycleSelection: {
- type: 'cycleSelection'
- }
- },
- text: {
- size: {
- type: 'number',
- min: 5,
- max: 128
- },
- bold: {
- type: 'toggle'
- },
- color: {
- type: 'button',
- selector: 'button.color-picker'
- },
- shadow: {
- type: 'button',
- selector: '#text-shadow-popup'
- }
- },
- grid: {
- opacity: {
- type: 'number',
- min: 0,
- max: 1,
- more: 0.01,
- less: -0.01,
- moreMore: 0.05,
- lessLess: -0.05,
- decimalPlaces: 2
- },
- size: {
- type: 'number',
- min: 16,
- max: 256
- },
- width: {
- type: 'number',
- min: 1,
- max: 5,
- more: 0.5,
- less: -0.5
- }
- },
- }
-
- function getScope() {
- return angular.element(document.getElementById('map-builder-view')).scope();
- }
-
- function getByPath(obj, path, separator = '.') {
- var parts = path.split('.');
- return parts.reduce((prev, curr) => prev && prev[curr], obj)
- }
-
- function setByPath(obj, path, value) {
- var parts = path.split('.');
- parts.reduce(function(prev, cur, idx) {
- if (idx == parts.length - 1) {
- prev[cur] = value;
- } else {
- return prev[cur] || {};
- }
- }, obj);
- }
-
- function changeScope(func) {
- let scope = getScope();
- scope.$apply(() => {
- func(scope);
- });
- }
-
- function getCurrentTool() {
- return getScope().tool;
- }
-
- function clampNumber(config, num) {
- const min = config.min || 0;
- const max = config.max || 128;
- return Math.min(Math.max(min, num), max);
- }
-
- function roundNumber(config, num) {
- const decimalPlaces = config.decimalPlaces || 1;
- const multiplier = Math.pow(10, decimalPlaces);
- return Math.round(num * multiplier) / multiplier;
- }
-
- function getStep(config, stepName) {
- return config[stepName] || defaultSteps[stepName];
- }
-
- let originalSelection = [];
- let curIdx = -1;
- function cycleSelection() {
- let scopeSelection = getScope().selected;
-
- if (scopeSelection.length <= 1 && curIdx === -1) {
- return;
- }
-
- if (curIdx === -1) {
- curIdx = 0;
- originalSelection = scopeSelection;
- changeScope(s => {
- s.selected = [originalSelection[curIdx]];
- });
- return;
- }
-
- if (originalSelection[curIdx] === scopeSelection[0]) {
- if (++curIdx >= originalSelection.length) {
- curIdx = -1;
- changeScope(s => {
- s.selected = originalSelection;
- });
- } else {
- changeScope(s => {
- s.selected = [originalSelection[curIdx]];
- });
- }
- } else {
- // user selected something different
- curIdx = -1;
- originalSelection = [];
- cycleSelection();
- }
- }
-
- function handleKeyForTool(tool, key) {
- const binds = keybinds[tool];
-
- const flatBinds = flattenObject(binds);
- for (const [path, bind] of Object.entries(flatBinds)) {
- if (key !== bind) {
- continue;
- }
-
- const pathArr = path.split('.');
- let propPath = tool + '.' + pathArr.slice(0, pathArr.length - 1).join('.');
- const val = pathArr[pathArr.length - 1];
-
- //console.log(`Found bind: ${propPath} with value ${val}`);
-
- const config = getByPath(configs, propPath) || {};
- if (config.alternateProperty) {
- propPath = config.alternateProperty;
- }
- switch (config.type) {
- case 'cycleSelection': {
- cycleSelection();
- break;
- }
- case 'toggle': {
- changeScope(scope => {
- const oldVal = getByPath(scope, propPath);
- setByPath(scope, propPath, !oldVal);
- });
- break;
- }
- case 'button': {
- //console.log(`Clicking button ${config.selector}`);
- $(config.selector).click();
- break;
- }
- case 'number': {
- changeScope(scope => {
- const oldVal = getByPath(scope, propPath);
- const step = getStep(config, val);
- const newVal = oldVal + step;
- const newValClamped = clampNumber(config, newVal);
- const rounded = roundNumber(config, newValClamped);
- //console.log(`Set ${propPath} to ${rounded}`);
- setByPath(scope, propPath, rounded);
- });
- break;
- }
- default:
- case 'select': {
- changeScope(scope => {
- setByPath(scope, propPath, val);
- });
- break;
- }
- }
- }
- }
-
- function handleZoom(action, altKey) {
- function move(direction, amount) {
- changeScope(s => { s.zoom.translate[direction] += amount * (altKey ? 3 : 1); });
- }
- function zoom(amount) {
- changeScope(s => {
- const newScale = s.zoom.scale * amount;
- s.zoom.scale = clampNumber({min: 1, max: 5}, roundNumber({decimalPlaces: 2}, newScale));
- });
- }
-
- const actions = {
- upleft: () => { move('y', 10); move('x', 10); },
- up: move.bind(null, 'y', 10),
- upright: () => { move('y', 10); move('x', -10); },
- right: move.bind(null, 'x', -10),
- downright: () => { move('y', -10); move('x', -10); },
- down: move.bind(null, 'y', -10),
- downleft: () => { move('y', -10); move('x', 10); },
- left: move.bind(null, 'x', 10),
- zoomIn: zoom.bind(null, 1.05),
- zoomOut: zoom.bind(null, 0.95),
- reset: () => {
- changeScope(s => {
- s.zoom.scale = 1;
- s.zoom.translate.x = 0;
- s.zoom.translate.y = 0;
- });
- }
- };
-
- actions[action]();
- }
-
- function keyHandler(e) {
- if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
- return;
- }
-
- if (keybinds.global.delete === e.key) {
- changeScope(s => {
- if (s.radialMenuActions().includes('delete')) {
- s.performRadialMenuAction('delete');
- }
- });
- return
- }
-
- const isZoomKey = Object.values(keybinds.global.zoom).includes(e.keyCode);
- if (isZoomKey) {
- const [zoomAction, _] = Object.entries(keybinds.global.zoom).find(elem => elem[1] === e.keyCode);
- handleZoom(zoomAction, e.altKey);
- return;
- }
- const isToolKey = Object.values(keybinds.global.tools).includes(e.key);
- if (isToolKey) {
- const [tool, _] = Object.entries(keybinds.global.tools).find(elem => elem[1] === e.key);
- changeScope(scope => scope.selectTool(tool));
- return;
- }
-
- // console.log(`Keypress: ${e.key}`);
- let cur = getCurrentTool();
- // console.log(`Cur tool: ${cur}`);
- if (!keybinds[cur]) {
- // console.log(`No Keybinds for tool "${getCurrentTool()}" configured.`);
- return;
- }
-
- handleKeyForTool(cur, e.key);
- }
-
-
- $(document).keydown(keyHandler);
- })();