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文字以内のものにしてください。

448 行
13KB

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