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.

509 lines
15KB

  1. import Utils from "/app/js/common/Utils.js";
  2. import GlobalEvents from "/app/js/common/EventCaller.js";
  3. import Input from "/app/js/ui/Input.js";
  4. import Renderer from "/app/js/ui/Renderer.js";
  5. import NESPalette from "/app/js/models/NESPalette.js";
  6. import ISurface from "/app/js/ifaces/ISurface.js";
  7. const EL_CANVAS_ID = "painter";
  8. /* --------------------------------------------------------------------
  9. * Univeral data and functions.
  10. ------------------------------------------------------------------- */
  11. var canvas = null;
  12. var context = null;
  13. var ctximg = null;
  14. function ResizeCanvasImg(w, h){
  15. if (canvas !== null){
  16. canvas.width = w;
  17. canvas.height = h;
  18. }
  19. };
  20. // Handling window resize events...
  21. var HANDLE_Resize = Utils.debounce(function(e){
  22. if (canvas !== null){
  23. ResizeCanvasImg(
  24. canvas.clientWidth,
  25. canvas.clientHeight
  26. );
  27. GlobalEvents.emit("resize", canvas.clientWidth, canvas.clientHeight);
  28. }
  29. }, 250);
  30. window.addEventListener("resize", HANDLE_Resize);
  31. // Setting-up Input controls.
  32. var input = new Input();
  33. input.enableKeyboardInput(true);
  34. input.preventDefaults = true;
  35. /* --------------------------------------------------------------------
  36. * CTRLPainter
  37. * Actual controlling class.
  38. ------------------------------------------------------------------- */
  39. // For reference...
  40. // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/stroke
  41. class CTRLPainter {
  42. constructor(){
  43. this.__scale = 1.0; // This is the scale the painter will display source information.
  44. this.__offset = [0.0, 0.0]; // This is the X,Y offset from origin to display source information.
  45. this.__onePaletteMode = true; // If true, ALL tiles will be drawing using the same palette.
  46. this.__brushSize = 1;
  47. this.__brushLastPos = [0.0, 0.0];
  48. this.__brushPos = [0.0, 0.0];
  49. this.__brushColor = 0;
  50. this.__brushPalette = 0;
  51. this.__gridEnabled = false;
  52. this.__gridSize = 1;
  53. this.__surface = null;
  54. this.__palette = null;
  55. this.__snapshotTriggered = false;
  56. // var self = this;
  57. var RenderD = Utils.throttle((function(){
  58. this.render();
  59. }).bind(this), 20);
  60. var LineToSurface = (function(x0, y0, x1, y1, ci, pi){
  61. var dx = x1 - x0;
  62. var ix = Math.sign(dx);
  63. dx = 2 * Math.abs(dx);
  64. var dy = y1 - y0;
  65. var iy = Math.sign(dy);
  66. dy = 2 * Math.abs(dy);
  67. if (dx > dy){
  68. var err = dy - (dx/2);
  69. var y = y0;
  70. Utils.range(x0, x1, 1).forEach((x) => {
  71. this.__surface.setColorIndex(x, y, ci, pi);
  72. if (err > 0 || (err == 0 && ix > 0)){
  73. err -= dx;
  74. y += iy;
  75. }
  76. err += dy;
  77. });
  78. } else {
  79. var err = dx - (dy/2);
  80. var x = x0;
  81. Utils.range(y0, y1, 1).forEach((y) => {
  82. this.__surface.setColorIndex(x, y, ci, pi);
  83. if (err > 0 || (err == 0 && iy > 0)){
  84. err -= dy;
  85. x += ix;
  86. }
  87. err += dx;
  88. });
  89. }
  90. }).bind(this);
  91. var handle_resize = (function(w,h){
  92. RenderD();
  93. }).bind(this);
  94. GlobalEvents.listen("resize", handle_resize);
  95. var handle_palinfochanged = (function(pdat){
  96. RenderD();
  97. }).bind(this);
  98. var handle_setapppalette = (function(pal){
  99. if (this.__palette !== null)
  100. this.__palette.unlisten("palettes_changed", handle_palinfochanged);
  101. this.__palette = pal;
  102. this.__palette.listen("palettes_changed", handle_palinfochanged);
  103. if (this.__surface !== null){
  104. this.__surface.palette = pal;
  105. if (this.__onePaletteMode === false)
  106. RenderD();
  107. }
  108. }).bind(this);
  109. GlobalEvents.listen("set_app_palette", handle_setapppalette);
  110. var handle_surface_data_changed = (function(){
  111. RenderD();
  112. }).bind(this);
  113. var handle_change_surface = (function(surf){
  114. if (surf !== null && !(surf instanceof ISurface)){
  115. console.log("WARNING: Attempted to set painter to non-surface instance.");
  116. return;
  117. }
  118. if (surf !== this.__surface){
  119. if (this.__surface !== null){
  120. this.__surface.unlisten("data_changed", handle_surface_data_changed);
  121. }
  122. this.__surface = surf;
  123. if (this.__surface !== null){
  124. this.__surface.listen("data_changed", handle_surface_data_changed);
  125. if (this.__palette === null && this.__surface.palette !== null){
  126. this.__palette = this.__surface.palette;
  127. } else if (this.__palette !== null && this.__surface.palette !== this.__palette){
  128. this.__surface.palette = this.__palette;
  129. }
  130. this.scale_to_fit();
  131. this.center_surface();
  132. }
  133. }
  134. RenderD();
  135. }).bind(this);
  136. GlobalEvents.listen("change_surface", handle_change_surface);
  137. var handle_color_change = (function(pi, ci){
  138. this.__brushPalette = pi;
  139. this.__brushColor = ci;
  140. }).bind(this);
  141. GlobalEvents.listen("active_palette_color", handle_color_change);
  142. var handle_mousemove = (function(e){
  143. this.__brushLastPos[0] = this.__brushPos[0];
  144. this.__brushLastPos[1] = this.__brushPos[1];
  145. this.__brushPos[0] = e.x;
  146. this.__brushPos[1] = e.y;
  147. if (this.__surface !== null){
  148. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  149. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  150. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  151. RenderD();
  152. }
  153. }
  154. }).bind(this);
  155. input.listen("mousemove", handle_mousemove);
  156. input.listen("mouseleft+mousemove", handle_mousemove);
  157. var handle_draw = (function(e){
  158. if (e.isCombo || e.button == 0){
  159. if (this.__surface !== null){
  160. //console.log(this.__brushPos);
  161. //console.log(this.__brushLastPos);
  162. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  163. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  164. var sx = (e.isCombo) ? Math.floor((this.__brushLastPos[0] - this.__offset[0]) * (1.0 / this.__scale)) : x;
  165. var sy = (e.isCombo) ? Math.floor((this.__brushLastPos[1] - this.__offset[1]) * (1.0 / this.__scale)) : y;
  166. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  167. if (!this.__snapshotTriggered){
  168. this.__snapshotTriggered = true;
  169. this.__surface.snapshot();
  170. }
  171. LineToSurface(sx, sy, x, y, this.__brushColor, this.__brushPalette);
  172. }
  173. }
  174. }
  175. }).bind(this);
  176. input.listen("mouseclick", handle_draw);
  177. input.listen("mouseleft+mousemove", handle_draw);
  178. var handle_mouseup = (function(e){
  179. this.__snapshotTriggered = false;
  180. }).bind(this);
  181. input.listen("mouseup", handle_mouseup);
  182. var handle_undo = (function(e){
  183. if (this.__surface !== null){
  184. this.__surface.undo();
  185. }
  186. }).bind(this);
  187. input.listen("ctrl+z", handle_undo);
  188. var handle_redo = (function(e){
  189. if (this.__surface !== null){
  190. this.__surface.redo();
  191. }
  192. }).bind(this);
  193. input.listen("ctrl+y", handle_redo);
  194. var handle_offset = (function(e){
  195. this.__offset[0] += e.x - e.lastX;
  196. this.__offset[1] += e.y - e.lastY;
  197. RenderD();
  198. }).bind(this);
  199. input.listen("shift+mouseleft+mousemove", handle_offset);
  200. var handle_scale = (function(e){
  201. if (e.delta < 0){
  202. this.scale_down();
  203. } else if (e.delta > 0){
  204. this.scale_up();
  205. }
  206. if (e.delta !== 0)
  207. RenderD();
  208. }).bind(this);
  209. input.listen("wheel", handle_scale);
  210. var elscale = document.querySelector("#painter_scale");
  211. if (elscale){
  212. var self = this;
  213. elscale.addEventListener("change", function(e){
  214. var val = Number(this.value);
  215. self.scale = val;
  216. RenderD();
  217. });
  218. elscale.value = this.__scale;
  219. }
  220. var handle_fittocanvas = (function(){
  221. this.scale_to_fit();
  222. this.center_surface();
  223. RenderD();
  224. }).bind(this);
  225. GlobalEvents.listen("painter-fit-to-canvas", handle_fittocanvas);
  226. var handle_togglegrid = (function(target, args){
  227. var elgridops = document.querySelector(".painter-grid-options");
  228. if (args.show){
  229. target.classList.add("pure-button-active");
  230. target.setAttribute("emit-args", JSON.stringify({show:false}));
  231. if (elgridops)
  232. elgridops.classList.remove("hidden");
  233. this.__gridEnabled = true;
  234. } else {
  235. target.classList.remove("pure-button-active");
  236. target.setAttribute("emit-args", JSON.stringify({show:true}));
  237. if (elgridops)
  238. elgridops.classList.add("hidden");
  239. this.__gridEnabled = false;
  240. }
  241. RenderD();
  242. }).bind(this);
  243. GlobalEvents.listen("painter-togglegrid", handle_togglegrid);
  244. var elgridops = document.querySelector(".painter-grid-options");
  245. if (elgridops){
  246. let self = this;
  247. elgridops.addEventListener("change", function(e){
  248. self.__gridSize = parseInt(this.value);
  249. if (self.__gridSize < 1 || self.__gridSize > 6)
  250. self.__gridSize = 1;
  251. RenderD();
  252. });
  253. }
  254. var handle_colormode = (function(target, args){
  255. if (args.onePaletteMode){
  256. target.classList.remove("pure-button-active");
  257. target.setAttribute("emit-args", JSON.stringify({onePaletteMode:false}));
  258. this.__onePaletteMode = true;
  259. } else {
  260. target.classList.add("pure-button-active");
  261. target.setAttribute("emit-args", JSON.stringify({onePaletteMode:true}));
  262. this.__onePaletteMode = false;
  263. }
  264. RenderD();
  265. }).bind(this);
  266. GlobalEvents.listen("painter-colormode", handle_colormode);
  267. }
  268. get onePaletteMode(){return this.__onePaletteMode;}
  269. set onePaletteMode(e){
  270. this.__onePaletteMode = (e === true);
  271. this.render();
  272. }
  273. get scale(){
  274. return this.__scale;
  275. }
  276. set scale(s){
  277. if (typeof(s) !== 'number')
  278. throw new TypeError("Expected number value.");
  279. this.__scale = Math.max(0.1, Math.min(100.0, s));
  280. var elscale = document.querySelector("#painter_scale");
  281. if (elscale){
  282. elscale.value = this.__scale;
  283. }
  284. }
  285. get showGrid(){return this.__gridEnabled;}
  286. set showGrid(e){
  287. this.__gridEnabled = (e === true);
  288. }
  289. initialize(){
  290. if (canvas === null){
  291. canvas = document.getElementById(EL_CANVAS_ID);
  292. if (!canvas)
  293. throw new Error("Failed to obtain the canvas element.");
  294. context = canvas.getContext("2d");
  295. if (!context)
  296. throw new Error("Failed to obtain canvas context.");
  297. context.imageSmoothingEnabled = false;
  298. ResizeCanvasImg(canvas.clientWidth, canvas.clientHeight); // A forced "resize".
  299. input.enableMouseInput(true, canvas);
  300. input.enableKeyboardInput(true);
  301. //input.mouseTargetElement = canvas;
  302. this.scale_to_fit();
  303. this.center_surface();
  304. }
  305. return this;
  306. }
  307. scale_up(amount=1){
  308. this.scale = this.scale + (amount*0.1);
  309. return this;
  310. }
  311. scale_down(amount=1){
  312. this.scale = this.scale - (amount*0.1);
  313. return this;
  314. }
  315. center_surface(){
  316. if (canvas === null || this.__surface === null)
  317. return;
  318. this.__offset[0] = Math.floor((canvas.clientWidth - (this.__surface.width * this.__scale)) * 0.5);
  319. this.__offset[1] = Math.floor((canvas.clientHeight - (this.__surface.height * this.__scale)) * 0.5);
  320. return this;
  321. }
  322. scale_to_fit(){
  323. if (canvas === null || this.__surface === null)
  324. return;
  325. var sw = canvas.clientWidth / this.__surface.width;
  326. var sh = canvas.clientHeight / this.__surface.height;
  327. var s = Math.min(sw, sh).toString();
  328. var di = s.indexOf(".");
  329. if (di >= 0)
  330. s = s.slice(0, di+2);
  331. this.scale = Number(s);
  332. }
  333. renderCursor(){
  334. if (context === null || this.__surface === null){return this;}
  335. // Draw the mouse position... if mouse is currently in bounds.
  336. if (input.isMouseInBounds()){
  337. context.save();
  338. context.fillStyle = "#AA9900";
  339. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0/this.__scale));
  340. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0/this.__scale));
  341. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  342. context.beginPath();
  343. context.rect(
  344. this.__offset[0] + (x*this.__scale),
  345. this.__offset[1] + (y*this.__scale),
  346. Math.ceil(this.__scale),
  347. Math.ceil(this.__scale)
  348. );
  349. context.fill();
  350. context.closePath();
  351. }
  352. context.restore();
  353. }
  354. return this;
  355. }
  356. renderGrid(){
  357. if (context === null || this.__surface === null || this.__gridEnabled === false){return this;}
  358. switch (this.__gridSize){
  359. case 1: // 8x8
  360. Renderer.renderGridNxN(
  361. context,
  362. 8,
  363. this.__surface.width, this.__surface.height,
  364. this.__scale, this.__offset,
  365. "#00FF00"
  366. ); break;
  367. case 2: // 16x8
  368. Renderer.renderGridMxN(
  369. context,
  370. 16, 8,
  371. this.__surface.width, this.__surface.height,
  372. this.__scale, this.__offset,
  373. "#00FF00"
  374. ); break;
  375. case 3: // 16x16
  376. Renderer.renderGridNxN(
  377. context,
  378. 16,
  379. this.__surface.width, this.__surface.height,
  380. this.__scale, this.__offset,
  381. "#00FF00"
  382. ); break;
  383. case 4:
  384. Renderer.renderGridNxN(
  385. context,
  386. 32,
  387. this.__surface.width, this.__surface.height,
  388. this.__scale, this.__offset,
  389. "#00FF00"
  390. ); break;
  391. case 5: // 16x16 Major | 8x8 Minor
  392. Renderer.renderGridMajorMinor(
  393. context,
  394. 16, 8,
  395. this.__surface.width, this.__surface.height,
  396. this.__scale, this.__offset,
  397. "#00FF00", "#008800"
  398. ); break;
  399. case 6: // 32x32 Major | 16x16 Minor
  400. Renderer.renderGridMajorMinor(
  401. context,
  402. 32, 16,
  403. this.__surface.width, this.__surface.height,
  404. this.__scale, this.__offset,
  405. "#00FF00", "#008800"
  406. ); break;
  407. }
  408. return this;
  409. }
  410. render(){
  411. if (context === null){return this;}
  412. if (this.__surface === null){
  413. Renderer.clear(context, NESPalette.Default[4]);
  414. return this;
  415. }
  416. // Drawing the surface to the canvas.
  417. Renderer.render(
  418. this.__surface,
  419. 0, 0,
  420. this.__surface.width, this.__surface.height,
  421. this.__scale,
  422. context,
  423. this.__offset[0], this.__offset[1],
  424. !this.__onePaletteMode
  425. );
  426. return this.renderCursor().renderGrid();
  427. }
  428. }
  429. const instance = new CTRLPainter();
  430. export default instance;