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

496 行
14KB

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