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.

701 lines
19KB

  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, mouse){
  192. var ename = "";
  193. mouse = (mouse === true);
  194. for (var i=0; i < codes.length; i++){
  195. if (mouse){
  196. switch(codes[i]){
  197. case 0:
  198. ename += ((ename !== "") ? "+" : "") + "mouseleft";
  199. break;
  200. case 1:
  201. ename += ((ename !== "") ? "+" : "") + "mouseright";
  202. break;
  203. case 2:
  204. ename += ((ename !== "") ? "+" : "") + "mousemiddle";
  205. break;
  206. case 8000:
  207. ename += ((ename !== "") ? "+" : "") + "mousemove";
  208. break;
  209. default:
  210. ename += ((ename !== "") ? "+" : "") + "mousebtn" + codes[i].toString();
  211. }
  212. } else {
  213. ename += ((ename !== "") ? "+" : "") + CodeToKeyName(codes[i]);
  214. }
  215. }
  216. return ename;
  217. }
  218. function KeymapEventName(){
  219. return CodesToEventName(KEYMAP["currentcodes"].map(e=>e[0]));
  220. }
  221. function ReorderEventName(ename){
  222. // This function takes a keyboard and mouse event name and reorders it into key-code order.
  223. // This way users can write the event any way they want, but should still result in proper
  224. // event being called.
  225. var elist = ename.split("+");
  226. // TODO: Need to test for duplicate event names for both keyboard and mouse event names.
  227. var ecodes = [];
  228. var mcodes = [];
  229. for (var i=0; i < elist.length; i++){
  230. var key = elist[i].trim().toLowerCase();
  231. // Check for mouse events first. These are hardcoded for now.
  232. if (key === "mouseleft"){
  233. mcodes.push(0);
  234. } else if (key === "mouseright"){
  235. mcodes.push(1);
  236. } else if (key === "mousemiddle"){
  237. mcodes.push(2);
  238. } else if (key.startsWith("mousebtn")){
  239. var sub = key.substring(8);
  240. if (!Number.isNaN(sub)){
  241. mcodes.push(parseInt(sub));
  242. } else {
  243. return ""; // This event name does not include valid mouse button code.
  244. }
  245. } else if (key === "mousemove"){
  246. mcodes.push(8000);
  247. }
  248. // Now handle keyboard event names.
  249. if (!(key in Object.keys(KEYBYNAME))){
  250. if (!Number.isNaN(key))
  251. ecodes.push(parseInt(key));
  252. else
  253. return ""; // This event name does not include valid key name!
  254. } else {
  255. ecodes.push(KEYBYNAME[key]);
  256. }
  257. }
  258. if (ecodes.length > 0){
  259. ecodes.sort(function(a, b){return a-b;});
  260. mcodes.sort(function(a, b){return a-b;});
  261. return CodesToEventName(ecodes) + CodesToEventName(mcodes);
  262. }
  263. return "";
  264. }
  265. export default class Input{
  266. constructor(){
  267. this.__emitter = new EventCaller();
  268. this.__preventDefaults = false;
  269. // Internet Explorer... that fudged up p.o.s. has to make mouse button detection difficult
  270. // with different button values from ALL other browsers. So... if Input has this value set to
  271. // true, then mouseup and mousedown events will base it's detection of IE button values, instead
  272. // of the real values.
  273. this.__ieMouseMode = false;
  274. // If set, this is the element that the mouse will focus on and adjust it's position against.
  275. this.__mouseTarget = null;
  276. this.__mousePosition = null;
  277. this.__mouseLastButton = -1;
  278. this.__mouseLastAction = "";
  279. this.__mouseButtons = [];
  280. this.__keyboardEnabled = false;
  281. this.__mouseEnabled = false;
  282. var buttonID = (function(e){
  283. var btn = e.button;
  284. if ((this.__ieMouseMode && btn === 1) || (!this.__ieMouseMode && btn === 0)){
  285. btn = 0;
  286. } else if ((this.__ieMouseMode && e.button === 4) || (!this.__ieMouseMode && e.button === 1)){
  287. btn = 1;
  288. }
  289. return btn;
  290. }).bind(this);
  291. var addMouseButton = (function(btn){
  292. if (this.__mouseButtons.findIndex(b=>b[0]===btn) < 0){
  293. this.__mouseButtons.push([btn, Math.floor(Date.now())]);
  294. return true;
  295. }
  296. return false;
  297. }).bind(this);
  298. var removeMouseButton = (function(btn){
  299. var idx = this.__mouseButtons.findIndex(b=>b[0]===btn);
  300. var diff = -1;
  301. if (idx >= 0){
  302. diff = Math.floor(Date.now()) - this.__mouseButtons[idx][1];
  303. this.__mouseButtons.splice(idx, 1);
  304. }
  305. return diff;
  306. }).bind(this);
  307. // ---------------------------------------------------------------------
  308. // Handling keyboard events.
  309. this.enableKeyboardInput = (function(){
  310. var handle_keydown = (function(e){
  311. if (AddToKeymap(e.keyCode, "keydown")){
  312. var ename = KeymapEventName();
  313. var edata = {
  314. source: this,
  315. iscombo: (ename.indexOf("+") >= 0),
  316. keys: ename,
  317. keycode:e.keyCode,
  318. keyname:CodeToKeyName(e.keyCode),
  319. action:"keydown"
  320. }
  321. this.__emitter.emit(ename, edata);
  322. this.__emitter.emit("keydown", edata);
  323. }
  324. }).bind(this);
  325. var handle_keyup = (function(e){
  326. var timediff = RemoveFromKeymap(e.keyCode, "keyup");
  327. if (timediff < 0){
  328. console.log("WARNING: Failed to find keycode '" + e.keyCode + "' in the Key Map.");
  329. } else {
  330. var ename = KeymapEventName();
  331. var edata = {
  332. source: this,
  333. iscombo: (ename.indexOf("+") >= 0),
  334. keys: ename,
  335. keycode: e.keyCode,
  336. keyname: CodeToKeyName(e.keyCode),
  337. action:"keyup"
  338. }
  339. if (timediff <= KEYPRESS_DELAY && KEYMAP["currentcodes"].length <= 0){
  340. this.__emitter.emit("keypress", edata);
  341. }
  342. this.__emitter.emit("keyup", edata);
  343. }
  344. }).bind(this);
  345. return (function(enable){
  346. enable = (enable !== false);
  347. // NOTE: There shouldn't be any harm if the user repeatedly enables or disables keyboard.
  348. if (enable){
  349. this.__keyboardEnabled = true;
  350. window.addEventListener("keydown", handle_keydown, false);
  351. window.addEventListener("keyup", handle_keyup, false);
  352. } else {
  353. this.__keyboardEnabled = false;
  354. window.removeEventListener("keydown", handle_keydown);
  355. window.removeEventListener("keyup", handle_keyup);
  356. }
  357. }).bind(this);
  358. }).apply(this);
  359. // ---------------------------------------------------------------------------------------
  360. // Handling mouse events.
  361. var MouseButtonsEventName = (function(){
  362. var e = "";
  363. for (var i=0; i < this.__mouseButtons.length; i++){
  364. e += (e !== "") ? "+" : "";
  365. switch (this.__mouseButtons[i]){
  366. case 0:
  367. e += "mouseleft";
  368. break;
  369. case 1:
  370. e += "mouseright";
  371. break;
  372. case 2:
  373. e += "mousemiddle";
  374. break;
  375. default:
  376. e += "mousebtn" + this.__mouseButtons[i].toString();
  377. }
  378. }
  379. return e;
  380. }).bind(this);
  381. // This function will only return an event name if keyboard
  382. var MouseEventName = (function(addon){
  383. var ename = KeymapEventName();
  384. var mname = MouseButtonsEventName();
  385. if (mname !== "")
  386. ename += ((ename !== "") ? "+" : "") + mname;
  387. if (typeof(addon) === "string")
  388. ename += ((ename !== "") ? "+" : "") + addon;
  389. return ename;
  390. }).bind(this);
  391. this.enableMouseInput = (function(){
  392. var mousePosition = (function(e){
  393. var pos = {
  394. lastX: (this.__mousePosition !== null) ? this.__mousePosition.x : null,
  395. lastY: (this.__mousePosition !== null) ? this.__mousePosition.y : null,
  396. x: e.clientX,
  397. y: e.clientY,
  398. inbounds: true
  399. }
  400. if (this.__mouseTarget !== null){
  401. var rect = this.__mouseTarget.getBoundingClientRect();
  402. pos.x -= rect.left;
  403. pos.y -= rect.top;
  404. pos.inbounds = (pos.x >= 0 && pos.x < rect.width && pos.y >= 0 && pos.y < rect.height);
  405. }
  406. pos.x = Math.floor(pos.x);
  407. pos.y = Math.floor(pos.y);
  408. return pos;
  409. }).bind(this);
  410. var handle_mousemove = (function(e){
  411. var pos = mousePosition(e);
  412. if (pos.inbounds){
  413. if (this.__preventDefaults){
  414. e.preventDefault();
  415. if (e.stopPropagation)
  416. e.stopPropagation();
  417. e.cancelBubble = true;
  418. }
  419. this.__mousePosition = pos;
  420. this.__mouseLastAction = "mousemove";
  421. var ename = MouseEventName();
  422. var data = {
  423. source: this,
  424. lastX: pos.lastX,
  425. lastY: pos.lastY,
  426. x: pos.x,
  427. y: pos.y,
  428. button: this.__mouseLastButton,
  429. action: "mousemove"
  430. };
  431. if (ename !== "" && ename !== "mousemove")
  432. this.__emitter.emit(ename, data);
  433. this.__emitter.emit("mousemove", data);
  434. }
  435. return false;
  436. }).bind(this);
  437. var handle_mousedown = (function(e){
  438. var button = buttonID(e);
  439. var pos = mousePosition(e);
  440. if (pos.inbounds){
  441. if (this.__preventDefaults){
  442. e.preventDefault();
  443. if (e.stopPropagation)
  444. e.stopPropagation();
  445. e.cancelBubble = true;
  446. }
  447. if (addMouseButton(button)){
  448. var ename = MouseEventName();
  449. var data = {
  450. source: this,
  451. lastX: pos.lastX,
  452. lastY: pos.lastY,
  453. x: pos.x,
  454. y: pos.y,
  455. button: button,
  456. action: "mousedown"
  457. };
  458. this.__mousePosition = pos;
  459. this.__mouseLastButton = button;
  460. this.__mouseLastAction = "mousedown";
  461. if (ename !== "")
  462. this.__emitter.emit(ename, data);
  463. this.__emitter.emit("mousedown", data);
  464. }
  465. }
  466. return false;
  467. }).bind(this);
  468. var handle_mouseup = (function(e){
  469. var button = buttonID(e);
  470. var pos = mousePosition(e);
  471. // NOTE: I still want to check for button removal, even before testing if an event should
  472. // fire, so that I don't have any phantom buttons listed as "pressed" in the mouseButtons list.
  473. var diff = removeMouseButton(button);
  474. if (pos.inbounds){
  475. if (this.__preventDefaults){
  476. e.preventDefault();
  477. if (e.stopPropagation)
  478. e.stopPropagation();
  479. e.cancelBubble = true;
  480. }
  481. if (diff >= 0){
  482. this.__mousePosition = pos;
  483. this.__mouseLastButton = button;
  484. this.__mouseLastAction = "mouseup";
  485. var data = {
  486. source: this,
  487. lastX: pos.lastX,
  488. lastY: pos.lastY,
  489. x: pos.x,
  490. y: pos.y,
  491. button: button,
  492. action: "mouseup"
  493. }
  494. this.__emitter.emit("mouseup", data);
  495. if (diff <= MOUSECLICK_DELAY && this.__mouseButtons.length <= 0){
  496. this.__emitter.emit("mouseclick", data);
  497. }
  498. }
  499. }
  500. return false;
  501. }).bind(this);
  502. var handle_mousewheel = (function(e){
  503. if (this.__preventDefaults)
  504. e.preventDefault();
  505. // TODO: Finish me!
  506. }).bind(this);
  507. // This event is purely for preventing Default behaviors on mouse events we're not using.
  508. var handle_mouseprevdef = (function(e){
  509. var pos = mousePosition(e);;
  510. if (this.__preventDefaults && (pos === null || pos.inbounds)){
  511. e.preventDefault();
  512. if (e.stopPropagation)
  513. e.stopPropagation();
  514. e.cancelBubble = true;
  515. }
  516. return false;
  517. }).bind(this);
  518. return (function(enable){
  519. enable = (enable !== false);
  520. // NOTE: There shouldn't be any harm if the user repeatedly enables or disables mouse.
  521. if (enable){
  522. this.__mouseEnabled = true;
  523. window.addEventListener("mousemove", handle_mousemove);
  524. window.addEventListener("mousedown", handle_mousedown);
  525. window.addEventListener("mouseup", handle_mouseup);
  526. window.addEventListener("mousewheel", handle_mousewheel);
  527. window.addEventListener("click", handle_mouseprevdef);
  528. window.addEventListener("dblclick", handle_mouseprevdef);
  529. window.addEventListener("contextmenu", handle_mouseprevdef);
  530. } else {
  531. this.__mouseEnabled = false;
  532. window.removeEventListener("mousemove", handle_mousemove);
  533. window.removeEventListener("mousedown", handle_mousedown);
  534. window.removeEventListener("mouseup", handle_mouseup);
  535. window.removeEventListener("mousewheel", handle_mousewheel);
  536. window.removeEventListener("click", handle_mouseprevdef);
  537. window.removeEventListener("dblclick", handle_mouseprevdef);
  538. window.removeEventListener("contextmenu", handle_mouseprevdef);
  539. }
  540. }).bind(this);
  541. }).apply(this);
  542. this.enableKeyboardInput();
  543. this.enableMouseInput();
  544. }
  545. get mouseInputEnabled(){return this.__mouseEnabled;}
  546. get keyboardInputEnabled(){return this.__keyboardEnabled;}
  547. get lastkey(){
  548. if (KEYMAP["lastcode"] !== null){
  549. if (KEYMAP["lastcode"] in Object.keys(KEYBYCODE)){
  550. return KEYBYCODE[KEYMAP["lastcode"]];
  551. }
  552. return "" + KEYMAP["lastcode"];
  553. }
  554. return "0";
  555. }
  556. get lastkeyaction(){
  557. return KEYMAP["lastaction"];
  558. }
  559. get currentKeys(){
  560. return KeymapEventName();
  561. }
  562. get currentKeyCodes(){
  563. return KEYMAP["currentcodes"].map(e=>e[0]);
  564. }
  565. get lastMouseAction(){
  566. return this.__mouseLastAction;
  567. }
  568. get lastMouseButton(){
  569. return this.__mouseLastButton;
  570. }
  571. get lastMousePosition(){
  572. if (this.__mousePosition === null || this.__mousePosition.lastX === null)
  573. return null;
  574. return {
  575. x: this.__mousePosition.lastX,
  576. y: this.__mousePosition.lastY
  577. };
  578. }
  579. get currentMousePosition(){
  580. if (this.__mousePosition === null)
  581. return null;
  582. return {
  583. x: this.__mousePosition.x,
  584. y: this.__mousePosition.y
  585. };
  586. }
  587. get preventDefaults(){return this.__preventDefaults;}
  588. set preventDefaults(p){
  589. this.__preventDefaults = (p === true);
  590. }
  591. get ieMouseMode(){return this.__ieMouseMode;}
  592. set ieMouseMode(m){this.__ieMouseMode = (m === true);}
  593. get mouseTargetElement(){return this.__mouseTarget;}
  594. set mouseTargetElement(el){
  595. if (el === null || Utils.isElement(el)){
  596. this.__mouseTarget = el;
  597. } else {
  598. throw new TypeError("Expected Mouse Target Element to be null or an HTMLElement object.");
  599. }
  600. }
  601. isKeyDown(key){
  602. if (typeof(key) === 'string'){
  603. key = KeyNameToCode(key);
  604. }
  605. for (var i=0; i < KEYMAP["currentcodes"].length; i++){
  606. if (KEYMAP["currentcodes"][i][0] === key){
  607. return true;
  608. }
  609. }
  610. return false;
  611. }
  612. listen(ename, func, owner=null, once=false){
  613. if ((["keyup", "keydown", "keypress", "mousemove", "mousedown", "mouseup", "mouseclick"]).indexOf(ename) >= 0){
  614. this.__emitter.listen(ename, func, owner, once);
  615. } else {
  616. ename = ReorderEventName(ename);
  617. if (ename === ""){
  618. throw new ValueError("Failed to parse key or key combination.");
  619. }
  620. this.__emitter.listen(ename, func, owner, once);
  621. }
  622. return this;
  623. }
  624. unlisten(ename, func, owner=null){
  625. if ((["keyup", "keydown", "keypress", "mousemove", "mousedown", "mouseup", "mouseclick"]).indexOf(ename) >= 0){
  626. this.__emitter.unlisten(ename, func, owner);
  627. } else {
  628. ename = ReorderEventName(ename);
  629. if (ename !== "")
  630. this.__emitter.unlisten(ename, func, owner);
  631. }
  632. return this;
  633. }
  634. }