A pixel art painter geared specifically at NES pixel art. Includes export for .chr binary file as well as palette and namespace data.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

509 行
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;