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个字符

CTRLPainter.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. // var self = this;
  56. var RenderD = Utils.throttle((function(){
  57. this.render();
  58. }).bind(this), 20);
  59. var LineToSurface = (function(x0, y0, x1, y1, ci, pi){
  60. var dx = x1 - x0;
  61. var ix = Math.sign(dx);
  62. dx = 2 * Math.abs(dx);
  63. var dy = y1 - y0;
  64. var iy = Math.sign(dy);
  65. dy = 2 * Math.abs(dy);
  66. if (dx > dy){
  67. var err = dy - (dx/2);
  68. var y = y0;
  69. Utils.range(x0, x1, 1).forEach((x) => {
  70. this.__surface.setColorIndex(x, y, ci, pi);
  71. if (err > 0 || (err == 0 && ix > 0)){
  72. err -= dx;
  73. y += iy;
  74. }
  75. err += dy;
  76. });
  77. } else {
  78. var err = dx - (dy/2);
  79. var x = x0;
  80. Utils.range(y0, y1, 1).forEach((y) => {
  81. this.__surface.setColorIndex(x, y, ci, pi);
  82. if (err > 0 || (err == 0 && iy > 0)){
  83. err -= dy;
  84. x += ix;
  85. }
  86. err += dx;
  87. });
  88. }
  89. }).bind(this);
  90. var handle_resize = (function(w,h){
  91. RenderD();
  92. }).bind(this);
  93. GlobalEvents.listen("resize", handle_resize);
  94. var handle_palinfochanged = (function(pdat){
  95. RenderD();
  96. }).bind(this);
  97. var handle_setapppalette = (function(pal){
  98. if (this.__palette !== null)
  99. this.__palette.unlisten("palettes_changed", handle_palinfochanged);
  100. this.__palette = pal;
  101. this.__palette.listen("palettes_changed", handle_palinfochanged);
  102. if (this.__surface !== null){
  103. this.__surface.palette = pal;
  104. if (this.__onePaletteMode === false)
  105. RenderD();
  106. }
  107. }).bind(this);
  108. GlobalEvents.listen("set_app_palette", handle_setapppalette);
  109. var handle_surface_data_changed = (function(){
  110. RenderD();
  111. }).bind(this);
  112. var handle_change_surface = (function(surf){
  113. if (!(surf instanceof ISurface)){
  114. console.log("WARNING: Attempted to set painter to non-surface instance.");
  115. return;
  116. }
  117. if (surf !== this.__surface){
  118. if (this.__surface !== null){
  119. this.__surface.unlisten("data_changed", handle_surface_data_changed);
  120. }
  121. this.__surface = surf;
  122. this.__surface.listen("data_changed", handle_surface_data_changed);
  123. if (this.__palette === null && this.__surface.palette !== null){
  124. this.__palette = this.__surface.palette;
  125. } else if (this.__palette !== null && this.__surface.palette !== this.__palette){
  126. this.__surface.palette = this.__palette;
  127. }
  128. this.scale_to_fit();
  129. this.center_surface();
  130. }
  131. RenderD();
  132. }).bind(this);
  133. GlobalEvents.listen("change_surface", handle_change_surface);
  134. var handle_color_change = (function(pi, ci){
  135. this.__brushPalette = pi;
  136. this.__brushColor = ci;
  137. }).bind(this);
  138. GlobalEvents.listen("active_palette_color", handle_color_change);
  139. var handle_mousemove = (function(e){
  140. this.__brushLastPos[0] = this.__brushPos[0];
  141. this.__brushLastPos[1] = this.__brushPos[1];
  142. this.__brushPos[0] = e.x;
  143. this.__brushPos[1] = e.y;
  144. if (this.__surface !== null){
  145. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  146. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  147. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  148. RenderD();
  149. }
  150. }
  151. }).bind(this);
  152. input.listen("mousemove", handle_mousemove);
  153. input.listen("mouseleft+mousemove", handle_mousemove);
  154. var handle_draw = (function(e){
  155. if (e.isCombo || e.button == 0){
  156. if (this.__surface !== null){
  157. //console.log(this.__brushPos);
  158. //console.log(this.__brushLastPos);
  159. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  160. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  161. var sx = (e.isCombo) ? Math.floor((this.__brushLastPos[0] - this.__offset[0]) * (1.0 / this.__scale)) : x;
  162. var sy = (e.isCombo) ? Math.floor((this.__brushLastPos[1] - this.__offset[1]) * (1.0 / this.__scale)) : y;
  163. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  164. LineToSurface(sx, sy, x, y, this.__brushColor, this.__brushPalette);
  165. //RenderD();
  166. }
  167. }
  168. }
  169. }).bind(this);
  170. input.listen("mouseclick", handle_draw);
  171. input.listen("mouseleft+mousemove", handle_draw);
  172. var handle_offset = (function(e){
  173. this.__offset[0] += e.x - e.lastX;
  174. this.__offset[1] += e.y - e.lastY;
  175. RenderD();
  176. }).bind(this);
  177. input.listen("shift+mouseleft+mousemove", handle_offset);
  178. var handle_scale = (function(e){
  179. if (e.delta < 0){
  180. this.scale_down();
  181. } else if (e.delta > 0){
  182. this.scale_up();
  183. }
  184. if (e.delta !== 0)
  185. RenderD();
  186. }).bind(this);
  187. input.listen("wheel", handle_scale);
  188. var elscale = document.querySelector("#painter_scale");
  189. if (elscale){
  190. var self = this;
  191. elscale.addEventListener("change", function(e){
  192. var val = Number(this.value);
  193. self.scale = val;
  194. RenderD();
  195. });
  196. elscale.value = this.__scale;
  197. }
  198. var handle_fittocanvas = (function(){
  199. this.scale_to_fit();
  200. this.center_surface();
  201. RenderD();
  202. }).bind(this);
  203. GlobalEvents.listen("painter-fit-to-canvas", handle_fittocanvas);
  204. var handle_togglegrid = (function(target, args){
  205. if (args.show){
  206. target.classList.add("pure-button-active");
  207. target.setAttribute("emit-args", JSON.stringify({show:false}));
  208. this.__gridEnabled = true;
  209. } else {
  210. target.classList.remove("pure-button-active");
  211. target.setAttribute("emit-args", JSON.stringify({show:true}));
  212. this.__gridEnabled = false;
  213. }
  214. RenderD();
  215. }).bind(this);
  216. GlobalEvents.listen("painter-togglegrid", handle_togglegrid);
  217. var handle_colormode = (function(target, args){
  218. if (args.onePaletteMode){
  219. target.classList.remove("pure-button-active");
  220. target.setAttribute("emit-args", JSON.stringify({onePaletteMode:false}));
  221. this.__onePaletteMode = true;
  222. } else {
  223. target.classList.add("pure-button-active");
  224. target.setAttribute("emit-args", JSON.stringify({onePaletteMode:true}));
  225. this.__onePaletteMode = false;
  226. }
  227. RenderD();
  228. }).bind(this);
  229. GlobalEvents.listen("painter-colormode", handle_colormode);
  230. }
  231. get onePaletteMode(){return this.__onePaletteMode;}
  232. set onePaletteMode(e){
  233. this.__onePaletteMode = (e === true);
  234. this.render();
  235. }
  236. get scale(){
  237. return this.__scale;
  238. }
  239. set scale(s){
  240. if (typeof(s) !== 'number')
  241. throw new TypeError("Expected number value.");
  242. this.__scale = Math.max(0.1, Math.min(100.0, s));
  243. var elscale = document.querySelector("#painter_scale");
  244. if (elscale){
  245. elscale.value = this.__scale;
  246. }
  247. }
  248. get showGrid(){return this.__gridEnabled;}
  249. set showGrid(e){
  250. this.__gridEnabled = (e === true);
  251. }
  252. initialize(){
  253. if (canvas === null){
  254. canvas = document.getElementById(EL_CANVAS_ID);
  255. if (!canvas)
  256. throw new Error("Failed to obtain the canvas element.");
  257. context = canvas.getContext("2d");
  258. if (!context)
  259. throw new Error("Failed to obtain canvas context.");
  260. context.imageSmoothingEnabled = false;
  261. ResizeCanvasImg(canvas.clientWidth, canvas.clientHeight); // A forced "resize".
  262. input.enableMouseInput(true, canvas);
  263. //input.mouseTargetElement = canvas;
  264. this.scale_to_fit();
  265. this.center_surface();
  266. }
  267. return this;
  268. }
  269. scale_up(amount=1){
  270. this.scale = this.scale + (amount*0.1);
  271. return this;
  272. }
  273. scale_down(amount=1){
  274. this.scale = this.scale - (amount*0.1);
  275. return this;
  276. }
  277. center_surface(){
  278. if (canvas === null || this.__surface === null)
  279. return;
  280. this.__offset[0] = Math.floor((canvas.clientWidth - (this.__surface.width * this.__scale)) * 0.5);
  281. this.__offset[1] = Math.floor((canvas.clientHeight - (this.__surface.height * this.__scale)) * 0.5);
  282. return this;
  283. }
  284. scale_to_fit(){
  285. if (canvas === null || this.__surface === null)
  286. return;
  287. var sw = canvas.clientWidth / this.__surface.width;
  288. var sh = canvas.clientHeight / this.__surface.height;
  289. var s = Math.min(sw, sh).toString();
  290. var di = s.indexOf(".");
  291. if (di >= 0)
  292. s = s.slice(0, di+2);
  293. this.scale = Number(s);
  294. }
  295. render(){
  296. if (context === null || this.__surface === null)
  297. return;
  298. // Drawing the surface to the canvas.
  299. Renderer.render(
  300. this.__surface,
  301. 0, 0,
  302. this.__surface.width, this.__surface.height,
  303. this.__scale,
  304. context,
  305. this.__offset[0], this.__offset[1],
  306. !this.__onePaletteMode
  307. );
  308. context.save();
  309. // Draw the mouse position... if mouse is currently in bounds.
  310. if (input.isMouseInBounds()){
  311. context.fillStyle = "#AA9900";
  312. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0/this.__scale));
  313. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0/this.__scale));
  314. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  315. context.beginPath();
  316. context.rect(
  317. this.__offset[0] + (x*this.__scale),
  318. this.__offset[1] + (y*this.__scale),
  319. Math.ceil(this.__scale),
  320. Math.ceil(this.__scale)
  321. );
  322. context.fill();
  323. context.closePath();
  324. }
  325. }
  326. // Draw grid.
  327. if (this.__gridEnabled && this.__scale > 0.5){
  328. context.strokeStyle = "#00FF00";
  329. var w = this.__surface.width * this.__scale;
  330. var h = this.__surface.height * this.__scale;
  331. var length = Math.max(this.__surface.width, this.__surface.height);
  332. for (var i=0; i < length; i += 8){
  333. var x = (i*this.__scale) + this.__offset[0];
  334. var y = (i*this.__scale) + this.__offset[1];
  335. if (i < this.__surface.width){
  336. context.beginPath();
  337. context.moveTo(x, this.__offset[1]);
  338. context.lineTo(x, this.__offset[1] + h);
  339. context.stroke();
  340. context.closePath();
  341. }
  342. if (i < this.__surface.height){
  343. context.beginPath();
  344. context.moveTo(this.__offset[0], y);
  345. context.lineTo(this.__offset[0] + w, y);
  346. context.stroke();
  347. context.closePath();
  348. }
  349. }
  350. }
  351. context.restore();
  352. return this;
  353. }
  354. }
  355. const instance = new CTRLPainter();
  356. export default instance;