A pixel art painter geared specifically at NES pixel art. Includes export for .chr binary file as well as palette and namespace data.
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.

610 lines
16KB

  1. import {EventCaller} from "/app/js/common/EventCaller.js";
  2. import Utils from "/app/js/common/Utils.js";
  3. // Keycode list based on...
  4. // https://keycode.info/
  5. var KEYBYCODE = {
  6. 3:"break",
  7. 8:"backspace",
  8. 9:"tab",
  9. 13:"enter",
  10. 16:"shift",
  11. 17:"ctrl",
  12. 18:"alt",
  13. 19:"pause",
  14. 20:"capslock",
  15. 27:"esc",
  16. 32:"space",
  17. 33:"pageup",
  18. 34:"pagedown",
  19. 35:"end",
  20. 36:"home",
  21. 37:"left",
  22. 38:"up",
  23. 39:"right",
  24. 40:"down",
  25. 41:"select",
  26. 42:"print",
  27. 43:"execute",
  28. 44:"printscreen",
  29. 45:"insert",
  30. 46:"delete",
  31. 47:"help",
  32. 48:"0",
  33. 49:"1",
  34. 50:"2",
  35. 51:"3",
  36. 52:"4",
  37. 53:"5",
  38. 54:"6",
  39. 55:"7",
  40. 56:"8",
  41. 57:"9",
  42. 65:"a",
  43. 66:"b",
  44. 67:"c",
  45. 68:"d",
  46. 69:"e",
  47. 70:"f",
  48. 71:"g",
  49. 72:"h",
  50. 73:"i",
  51. 74:"j",
  52. 75:"k",
  53. 76:"l",
  54. 77:"m",
  55. 78:"n",
  56. 79:"o",
  57. 80:"p",
  58. 81:"q",
  59. 82:"r",
  60. 83:"s",
  61. 84:"t",
  62. 85:"u",
  63. 86:"v",
  64. 87:"w",
  65. 88:"x",
  66. 89:"y",
  67. 90:"z",
  68. 91:"leftmod", // Window key (left)
  69. 92:"rightwin",// Window key (right)
  70. 93:"rightmod",// Window key (right)
  71. 96:"num0",
  72. 97:"num1",
  73. 98:"num2",
  74. 99:"num3",
  75. 100:"num4",
  76. 101:"num5",
  77. 102:"num6",
  78. 103:"num7",
  79. 104:"num8",
  80. 105:"num9",
  81. 112:"f1",
  82. 113:"f2",
  83. 114:"f3",
  84. 115:"f4",
  85. 116:"f5",
  86. 117:"f6",
  87. 118:"f7",
  88. 119:"f8",
  89. 120:"f9",
  90. 121:"f10",
  91. 122:"f11",
  92. 123:"f12",
  93. 144:"numlock",
  94. 145:"scrolllock",
  95. };
  96. var KEYBYNAME = (function(){
  97. var keys = Object.keys(KEYBYCODE);
  98. var o = {};
  99. for (var i=0; i < keys.length; i++){
  100. if (KEYBYCODE.hasOwnProperty(keys[i])){
  101. o[KEYBYCODE[keys[i]]] = keys[i];
  102. }
  103. }
  104. return o;
  105. })();
  106. var KEYTYPE = {
  107. "number":[48,49,50,51,52,53,54,55,56,57,96,97,98,99,100,101,102,103,104,105],
  108. "letter":[65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90],
  109. "mod":[16,17,18],
  110. "arrow":[37,38,39,40],
  111. "wasd":[87,65,83,68],
  112. "fn":[112,113,114,115,116,117,118,119,120,121,122,123],
  113. "n1":[48,96],
  114. "n2":[49,97],
  115. "n3":[50,98],
  116. "n4":[51,99],
  117. "n5":[52,100],
  118. "n6":[53,101],
  119. "n7":[54,102],
  120. "n8":[55,103],
  121. "n9":[56,104],
  122. "n0":[57,105]
  123. };
  124. var KEYMAP = {
  125. "lastcode":null,
  126. "lastaction":"",
  127. "currentcodes":[]
  128. };
  129. var MOUSEBYCODE = {
  130. 0: "mbl", // (M)ouse (B)utton (L)eft
  131. 1: "mbm", // (M)ouse (B)utton (M)iddle
  132. 2: "mbr" // (M)ouse (B)utton (R)ight
  133. };
  134. var MOUSEBYNAME = {
  135. "mbl": 0,
  136. "mbm": 1,
  137. "mbr": 2
  138. };
  139. // TODO: Reeval this idea.
  140. const KEYPRESS_DELAY = 350; // Time in milliseconds. NOTE: May make this a variable in future.
  141. const MOUSECLICK_DELAY = 350; // Time in milliseconds.
  142. function AssignCodeName(code, name){
  143. name = name.toLowerCase();
  144. var remove = (code in Object.keys(KEYBYCODE));
  145. if (name in Object.keys(KEYBYNAME)){
  146. if (remove && KEYBYCODE[code] === name){
  147. return; // We're being asked to replace the exact same thing. SKIP!
  148. }
  149. throw new ValueError("Key name '" + name + "' already assigned. Cannot use duplicate key names.");
  150. }
  151. if (remove){
  152. delete KEYBYNAME[KEYBYCODE[code]];
  153. }
  154. KEYBYCODE[code] = name;
  155. KEYBYNAME[name] = code;
  156. }
  157. function KeymapContains(code){
  158. return KEYMAP["currentcodes"].findIndex(c=>c[0] == code) >= 0;
  159. }
  160. function AddToKeymap(code, action){
  161. KEYMAP["lastcode"] = code;
  162. KEYMAP["lastaction"] = action;
  163. if (KeymapContains(code) == false){
  164. KEYMAP["currentcodes"].push([code, Math.floor(Date.now())]);
  165. if (KEYMAP["currentcodes"].length > 1){
  166. KEYMAP["currentcodes"].sort(function(a, b){return a[0] - b[0];});
  167. }
  168. return true;
  169. }
  170. return false;
  171. }
  172. function RemoveFromKeymap(code, action){
  173. KEYMAP["lastcode"] = code;
  174. KEYMAP["lastaction"] = action;
  175. var ctime = Math.floor(Date.now());
  176. for (var i=0; i < KEYMAP["currentcodes"].length; i++){
  177. if (KEYMAP["currentcodes"][i][0] === code){
  178. var timediff = ctime - KEYMAP["currentcodes"][i][1];
  179. KEYMAP["currentcodes"].splice(i, 1);
  180. return timediff;
  181. }
  182. }
  183. return -1;
  184. }
  185. function KeyNameToCode(key){
  186. return (key in Object.keys(KEYBYNAME)) ? KEYBYNAME[key] : -1;
  187. }
  188. function CodeToKeyName(code){
  189. return (code in Object.keys(KEYBYCODE)) ? KEYBYCODE[code] : "" + code;
  190. }
  191. function CodesToEventName(codes){
  192. var ename = "";
  193. for (var i=0; i < codes.length; i++){
  194. ename += ((ename !== "") ? "+" : "") + CodeToKeyName(codes[i]);
  195. }
  196. return ename;
  197. }
  198. function KeymapEventName(){
  199. return CodesToEventName(KEYMAP["currentcodes"].map(e=>e[0]));
  200. }
  201. function ReorderEventName(ename){
  202. // This function takes a keyboard event name and reorders it into key-code order.
  203. // This way users can write the event any way they want, but should still result in proper
  204. // event being called.
  205. var elist = ename.split("+");
  206. var ecodes = [];
  207. for (var i=0; i < elist.length; i++){
  208. var key = elist[i].trim().toLowerCase();
  209. if (!(key in Object.keys(KEYBYNAME))){
  210. if (!Number.isNaN(key))
  211. ecodes.push(parseInt(key));
  212. else
  213. return ""; // This event name does not include valid key name!
  214. } else {
  215. ecodes.push(KEYBYNAME[key]);
  216. }
  217. }
  218. if (ecodes.length > 0){
  219. ecodes.sort(function(a, b){return a-b;});
  220. return CodesToEventName(ecodes);
  221. }
  222. return "";
  223. }
  224. export default class Input{
  225. constructor(){
  226. this.__emitter = new EventCaller();
  227. this.__preventDefaults = false;
  228. // Internet Explorer... that fudged up p.o.s. has to make mouse button detection difficult
  229. // with different button values from ALL other browsers. So... if Input has this value set to
  230. // true, then mouseup and mousedown events will base it's detection of IE button values, instead
  231. // of the real values.
  232. this.__ieMouseMode = false;
  233. // If set, this is the element that the mouse will focus on and adjust it's position against.
  234. this.__mouseTarget = null;
  235. this.__mousePosition = null;
  236. this.__mouseLastButton = -1;
  237. this.__mouseLastAction = "";
  238. this.__mouseButtons = [];
  239. this.__keyboardEnabled = false;
  240. this.__mouseEnabled = false;
  241. this.enableKeyboardInput = (function(){
  242. var handle_keydown = (function(e){
  243. if (AddToKeymap(e.keyCode, "keydown")){
  244. var ename = KeymapEventName();
  245. var edata = {
  246. source: this,
  247. iscombo: (ename.indexOf("+") >= 0),
  248. keys: ename,
  249. keycode:e.keyCode,
  250. keyname:CodeToKeyName(e.keyCode),
  251. action:"keydown"
  252. }
  253. this.__emitter.emit(ename, edata);
  254. this.__emitter.emit("keydown", edata);
  255. }
  256. }).bind(this);
  257. var handle_keyup = (function(e){
  258. var timediff = RemoveFromKeymap(e.keyCode, "keyup");
  259. if (timediff < 0){
  260. console.log("WARNING: Failed to find keycode '" + e.keyCode + "' in the Key Map.");
  261. } else {
  262. var ename = KeymapEventName();
  263. var edata = {
  264. source: this,
  265. iscombo: (ename.indexOf("+") >= 0),
  266. keys: ename,
  267. keycode: e.keyCode,
  268. keyname: CodeToKeyName(e.keyCode),
  269. action:"keyup"
  270. }
  271. if (timediff <= KEYPRESS_DELAY && KEYMAP["currentcodes"].length <= 0){
  272. this.__emitter.emit("keypress", edata);
  273. }
  274. this.__emitter.emit("keyup", edata);
  275. }
  276. }).bind(this);
  277. return (function(enable){
  278. enable = (enable !== false);
  279. // NOTE: There shouldn't be any harm if the user repeatedly enables or disables keyboard.
  280. if (enable){
  281. this.__keyboardEnabled = true;
  282. window.addEventListener("keydown", handle_keydown, false);
  283. window.addEventListener("keyup", handle_keyup, false);
  284. } else {
  285. this.__keyboardEnabled = false;
  286. window.removeEventListener("keydown", handle_keydown);
  287. window.removeEventListener("keyup", handle_keyup);
  288. }
  289. }).bind(this);
  290. }).apply(this);
  291. this.enableMouseInput = (function(){
  292. var mousePosition = (function(e){
  293. var pos = {
  294. lastX: (this.__mousePosition !== null) ? this.__mousePosition.x : null,
  295. lastY: (this.__mousePosition !== null) ? this.__mousePosition.y : null,
  296. x: e.clientX,
  297. y: e.clientY,
  298. inbounds: true
  299. }
  300. if (this.__mouseTarget !== null){
  301. var rect = this.__mouseTarget.getBoundingClientRect();
  302. pos.x -= rect.left;
  303. pos.y -= rect.top;
  304. pos.inbounds = (pos.x >= 0 && pos.x < rect.width && pos.y >= 0 && pos.y < rect.height);
  305. }
  306. pos.x = Math.floor(pos.x);
  307. pos.y = Math.floor(pos.y);
  308. return pos;
  309. }).bind(this);
  310. var buttonID = (function(e){
  311. var btn = e.button;
  312. if ((this.__ieMouseMode && btn === 1) || (!this.__ieMouseMode && btn === 0)){
  313. btn = 0;
  314. } else if ((this.__ieMouseMode && e.button === 4) || (!this.__ieMouseMode && e.button === 1)){
  315. btn = 1;
  316. }
  317. return btn;
  318. }).bind(this);
  319. var addMouseButton = (function(btn){
  320. if (this.__mouseButtons.findIndex(b=>b[0]===btn) < 0){
  321. this.__mouseButtons.push([btn, Math.floor(Date.now())]);
  322. return true;
  323. }
  324. return false;
  325. }).bind(this);
  326. var removeMouseButton = (function(btn){
  327. var idx = this.__mouseButtons.findIndex(b=>b[0]===btn);
  328. var diff = -1;
  329. if (idx >= 0){
  330. diff = Math.floor(Date.now()) - this.__mouseButtons[idx][1];
  331. this.__mouseButtons.splice(idx, 1);
  332. }
  333. return diff;
  334. }).bind(this);
  335. var handle_mousemove = (function(e){
  336. var pos = mousePosition(e);
  337. if (pos.inbounds){
  338. if (this.__preventDefaults){
  339. e.preventDefault();
  340. if (e.stopPropagation)
  341. e.stopPropagation();
  342. e.cancelBubble = true;
  343. }
  344. this.__mousePosition = pos;
  345. this.__mouseLastAction = "mousemove";
  346. this.__emitter.emit("mousemove", {
  347. source: this,
  348. lastX: pos.lastX,
  349. lastY: pos.lastY,
  350. x: pos.x,
  351. y: pos.y,
  352. button: this.__mouseLastButton,
  353. action: "mousemove"
  354. });
  355. }
  356. return false;
  357. }).bind(this);
  358. var handle_mousedown = (function(e){
  359. var button = buttonID(e);
  360. var pos = mousePosition(e);
  361. if (pos.inbounds){
  362. if (this.__preventDefaults){
  363. e.preventDefault();
  364. if (e.stopPropagation)
  365. e.stopPropagation();
  366. e.cancelBubble = true;
  367. }
  368. if (addMouseButton(button)){
  369. this.__mousePosition = pos;
  370. this.__mouseLastButton = button;
  371. this.__mouseLastAction = "mousedown";
  372. this.__emitter.emit("mousedown", {
  373. source: this,
  374. lastX: pos.lastX,
  375. lastY: pos.lastY,
  376. x: pos.x,
  377. y: pos.y,
  378. button: button,
  379. action: "mousedown"
  380. });
  381. }
  382. }
  383. return false;
  384. }).bind(this);
  385. var handle_mouseup = (function(e){
  386. var button = buttonID(e);
  387. var pos = mousePosition(e);
  388. // NOTE: I still want to check for button removal, even before testing if an event should
  389. // fire, so that I don't have any phantom buttons listed as "pressed" in the mouseButtons list.
  390. var diff = removeMouseButton(button);
  391. if (pos.inbounds){
  392. if (this.__preventDefaults){
  393. e.preventDefault();
  394. if (e.stopPropagation)
  395. e.stopPropagation();
  396. e.cancelBubble = true;
  397. }
  398. if (diff >= 0){
  399. this.__mousePosition = pos;
  400. this.__mouseLastButton = button;
  401. this.__mouseLastAction = "mouseup";
  402. var data = {
  403. source: this,
  404. lastX: pos.lastX,
  405. lastY: pos.lastY,
  406. x: pos.x,
  407. y: pos.y,
  408. button: button,
  409. action: "mouseup"
  410. }
  411. this.__emitter.emit("mouseup", data);
  412. if (diff <= MOUSECLICK_DELAY && this.__mouseButtons.length <= 0){
  413. this.__emitter.emit("mouseclick", data);
  414. }
  415. }
  416. }
  417. return false;
  418. }).bind(this);
  419. var handle_mousewheel = (function(e){
  420. if (this.__preventDefaults)
  421. e.preventDefault();
  422. // TODO: Finish me!
  423. }).bind(this);
  424. // This event is purely for preventing Default behaviors on mouse events we're not using.
  425. var handle_mouseprevdef = (function(e){
  426. var pos = mousePosition(e);;
  427. if (this.__preventDefaults && (pos === null || pos.inbounds)){
  428. e.preventDefault();
  429. if (e.stopPropagation)
  430. e.stopPropagation();
  431. e.cancelBubble = true;
  432. }
  433. return false;
  434. }).bind(this);
  435. return (function(enable){
  436. enable = (enable !== false);
  437. // NOTE: There shouldn't be any harm if the user repeatedly enables or disables mouse.
  438. if (enable){
  439. this.__mouseEnabled = true;
  440. window.addEventListener("mousemove", handle_mousemove);
  441. window.addEventListener("mousedown", handle_mousedown);
  442. window.addEventListener("mouseup", handle_mouseup);
  443. window.addEventListener("mousewheel", handle_mousewheel);
  444. window.addEventListener("click", handle_mouseprevdef);
  445. window.addEventListener("dblclick", handle_mouseprevdef);
  446. window.addEventListener("contextmenu", handle_mouseprevdef);
  447. } else {
  448. this.__mouseEnabled = false;
  449. window.removeEventListener("mousemove", handle_mousemove);
  450. window.removeEventListener("mousedown", handle_mousedown);
  451. window.removeEventListener("mouseup", handle_mouseup);
  452. window.removeEventListener("mousewheel", handle_mousewheel);
  453. window.removeEventListener("click", handle_mouseprevdef);
  454. window.removeEventListener("dblclick", handle_mouseprevdef);
  455. window.removeEventListener("contextmenu", handle_mouseprevdef);
  456. }
  457. }).bind(this);
  458. }).apply(this);
  459. this.enableKeyboardInput();
  460. this.enableMouseInput();
  461. }
  462. get mouseInputEnabled(){return this.__mouseEnabled;}
  463. get keyboardInputEnabled(){return this.__keyboardEnabled;}
  464. get lastkey(){
  465. if (KEYMAP["lastcode"] !== null){
  466. if (KEYMAP["lastcode"] in Object.keys(KEYBYCODE)){
  467. return KEYBYCODE[KEYMAP["lastcode"]];
  468. }
  469. return "" + KEYMAP["lastcode"];
  470. }
  471. return "0";
  472. }
  473. get lastkeyaction(){
  474. return KEYMAP["lastaction"];
  475. }
  476. get currentKeys(){
  477. return KeymapEventName();
  478. }
  479. get currentKeyCodes(){
  480. return KEYMAP["currentcodes"].map(e=>e[0]);
  481. }
  482. get lastMouseAction(){
  483. return this.__mouseLastAction;
  484. }
  485. get lastMouseButton(){
  486. return this.__mouseLastButton;
  487. }
  488. get lastMousePosition(){
  489. if (this.__mousePosition === null || this.__mousePosition.lastX === null)
  490. return null;
  491. return {
  492. x: this.__mousePosition.lastX,
  493. y: this.__mousePosition.lastY
  494. };
  495. }
  496. get currentMousePosition(){
  497. if (this.__mousePosition === null)
  498. return null;
  499. return {
  500. x: this.__mousePosition.x,
  501. y: this.__mousePosition.y
  502. };
  503. }
  504. get preventDefaults(){return this.__preventDefaults;}
  505. set preventDefaults(p){
  506. this.__preventDefaults = (p === true);
  507. }
  508. get ieMouseMode(){return this.__ieMouseMode;}
  509. set ieMouseMode(m){this.__ieMouseMode = (m === true);}
  510. get mouseTargetElement(){return this.__mouseTarget;}
  511. set mouseTargetElement(el){
  512. if (el === null || Utils.isElement(el)){
  513. this.__mouseTarget = el;
  514. } else {
  515. throw new TypeError("Expected Mouse Target Element to be null or an HTMLElement object.");
  516. }
  517. }
  518. isKeyDown(key){
  519. if (typeof(key) === 'string'){
  520. key = KeyNameToCode(key);
  521. }
  522. for (var i=0; i < KEYMAP["currentcodes"].length; i++){
  523. if (KEYMAP["currentcodes"][i][0] === key){
  524. return true;
  525. }
  526. }
  527. return false;
  528. }
  529. listen(ename, func, owner=null, once=false){
  530. if ((["keyup", "keydown", "keypress", "mousemove", "mousedown", "mouseup", "mouseclick"]).indexOf(ename) >= 0){
  531. this.__emitter.listen(ename, func, owner, once);
  532. } else {
  533. ename = ReorderEventName(ename);
  534. if (ename === ""){
  535. throw new ValueError("Failed to parse key or key combination.");
  536. }
  537. this.__emitter.listen(ename, func, owner, once);
  538. }
  539. return this;
  540. }
  541. unlisten(ename, func, owner=null){
  542. if ((["keyup", "keydown", "keypress", "mousemove", "mousedown", "mouseup", "mouseclick"]).indexOf(ename) >= 0){
  543. this.__emitter.unlisten(ename, func, owner);
  544. } else {
  545. ename = ReorderEventName(ename);
  546. if (ename !== "")
  547. this.__emitter.unlisten(ename, func, owner);
  548. }
  549. return this;
  550. }
  551. }