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

394 行
11KB

  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 = true; //false;
  99. this.__gridSize = 1;
  100. this.__surface = null;
  101. // var self = this;
  102. var RenderD = Utils.throttle((function(){
  103. this.render();
  104. }).bind(this), 20);
  105. var LineToSurface = (function(x0, y0, x1, y1, ci, pi){
  106. var dx = x1 - x0;
  107. var dy = y1 - y0;
  108. if (dx == 0){
  109. // Verticle line
  110. var x = x0;
  111. var s = Math.min(y0, y1);
  112. var e = Math.max(y0, y1);
  113. for (var y = s; y <= e; y++){
  114. this.__surface.setColorIndex(x, y, ci, pi);
  115. }
  116. } else {
  117. var slope = Math.abs(dy/dx);
  118. var err = 0.0;
  119. var y = y0;
  120. var surf = this.__surface;
  121. Utils.range(x0, x1, 1).forEach(function(x){
  122. surf.setColorIndex(Math.floor(x), Math.floor(y), ci, pi);
  123. err += slope;
  124. if (err > 0.5){
  125. y += Math.sign(dy);
  126. err -= 1.0;
  127. }
  128. });
  129. }
  130. }).bind(this);
  131. var handle_resize = (function(w,h){
  132. RenderD();
  133. }).bind(this);
  134. GlobalEvents.listen("resize", handle_resize);
  135. var handle_change_surface = (function(surf){
  136. if (!(surf instanceof ISurface)){
  137. console.log("WARNING: Attempted to set painter to non-surface instance.");
  138. return;
  139. }
  140. this.__surface = surf;
  141. this.center_surface();
  142. RenderD();
  143. }).bind(this);
  144. GlobalEvents.listen("change_surface", handle_change_surface);
  145. var handle_color_change = (function(pi, ci){
  146. this.__brushPalette = pi;
  147. this.__brushColor = ci;
  148. }).bind(this);
  149. GlobalEvents.listen("active_palette_color", handle_color_change);
  150. var handle_mousemove = (function(e){
  151. this.__brushLastPos[0] = this.__brushPos[0];
  152. this.__brushLastPos[1] = this.__brushPos[1];
  153. this.__brushPos[0] = e.x;
  154. this.__brushPos[1] = e.y;
  155. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  156. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  157. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  158. RenderD();
  159. }
  160. }).bind(this);
  161. input.listen("mousemove", handle_mousemove);
  162. input.listen("mouseleft+mousemove", handle_mousemove);
  163. var handle_draw = (function(e){
  164. if (e.isCombo || e.button == 0){
  165. if (this.__surface !== null){
  166. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0 / this.__scale));
  167. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0 / this.__scale));
  168. var sx = (e.isCombo) ? Math.floor((this.__brushLastPos[0] - this.__offset[0]) * (1.0 / this.__scale)) : x;
  169. var sy = (e.isCombo) ? Math.floor((this.__brushLastPos[1] - this.__offset[1]) * (1.0 / this.__scale)) : y;
  170. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  171. LineToSurface(sx, sy, x, y, this.__brushColor, this.__brushPalette);
  172. //this.__surface.setColorIndex(x, y, this.__brushColor, this.__brushPalette);
  173. RenderD();
  174. }
  175. }
  176. }
  177. }).bind(this);
  178. input.listen("mouseclick", handle_draw);
  179. input.listen("mouseleft+mousemove", handle_draw);
  180. var handle_offset = (function(e){
  181. this.__offset[0] += e.x - e.lastX;
  182. this.__offset[1] += e.y - e.lastY;
  183. RenderD();
  184. }).bind(this);
  185. input.listen("shift+mouseleft+mousemove", handle_offset);
  186. var handle_scale = (function(e){
  187. if (e.delta < 0){
  188. this.scale_down();
  189. } else if (e.delta > 0){
  190. this.scale_up();
  191. }
  192. if (e.delta !== 0)
  193. RenderD();
  194. }).bind(this);
  195. input.listen("wheel", handle_scale);
  196. }
  197. get onePaletteMode(){return this.__onePaletteMode;}
  198. set onePaletteMode(e){
  199. this.__onePaletteMode = (e === true);
  200. this.render();
  201. }
  202. get scale(){
  203. return this.__scale;
  204. }
  205. set scale(s){
  206. if (typeof(s) !== 'number')
  207. throw new TypeError("Expected number value.");
  208. this.__scale = Math.max(0.1, Math.min(100.0, s));
  209. }
  210. get showGrid(){return this.__gridEnabled;}
  211. set showGrid(e){
  212. this.__gridEnabled = (e === true);
  213. }
  214. initialize(){
  215. if (canvas === null){
  216. canvas = document.getElementById(EL_CANVAS_ID);
  217. if (!canvas)
  218. throw new Error("Failed to obtain the canvas element.");
  219. context = canvas.getContext("2d");
  220. if (!context)
  221. throw new Error("Failed to obtain canvas context.");
  222. context.imageSmoothingEnabled = false;
  223. ResizeCanvasImg(canvas.clientWidth, canvas.clientHeight); // A forced "resize".
  224. input.mouseTargetElement = canvas;
  225. this.center_surface();
  226. }
  227. return this;
  228. }
  229. scale_up(amount=1){
  230. this.scale = this.scale + (amount*0.1);
  231. return this;
  232. }
  233. scale_down(amount=1){
  234. this.scale = this.scale - (amount*0.1);
  235. return this;
  236. }
  237. center_surface(){
  238. if (canvas === null || this.__surface === null)
  239. return;
  240. this.__offset[0] = Math.floor((canvas.clientWidth - this.__surface.width) * 0.5);
  241. this.__offset[1] = Math.floor((canvas.clientHeight - this.__surface.height) * 0.5);
  242. return this;
  243. }
  244. render(){
  245. if (context === null || this.__surface === null)
  246. return;
  247. context.save();
  248. // Clearing the context surface...
  249. context.fillStyle = NESPalette.Default[4];
  250. context.fillRect(
  251. 0,0,
  252. Math.floor(canvas.clientWidth),
  253. Math.floor(canvas.clientHeight)
  254. );
  255. OpenCanvasPixels();
  256. for (var j = 0; j < this.__surface.height; j++){
  257. var y = (j*this.__scale) + this.__offset[1];
  258. for (var i = 0; i < this.__surface.width; i++){
  259. var x = (i*this.__scale) + this.__offset[0];
  260. if (x >= 0 && x < canvas.clientWidth && y >= 0 && y < canvas.clientHeight){
  261. var color = NESPalette.Default[4];
  262. if (this.__onePaletteMode){
  263. var pinfo = this.__surface.getColorIndex(i, j);
  264. if (pinfo.ci >= 0)
  265. color = NESPalette.Default[pinfo.ci];
  266. } else {
  267. color = this.__surface.getColor(i, j);
  268. }
  269. PutCanvasPixel(x,y, this.__scale, color);
  270. }
  271. }
  272. }
  273. CloseCanvasPixels();
  274. // Draw the mouse position... if mouse is currently in bounds.
  275. if (input.isMouseInBounds()){
  276. context.fillStyle = "#AA9900";
  277. var x = Math.floor((this.__brushPos[0] - this.__offset[0]) * (1.0/this.__scale));
  278. var y = Math.floor((this.__brushPos[1] - this.__offset[1]) * (1.0/this.__scale));
  279. if (x >= 0 && x < this.__surface.width && y >= 0 && y < this.__surface.height){
  280. context.beginPath();
  281. context.rect(
  282. this.__offset[0] + (x*this.__scale),
  283. this.__offset[1] + (y*this.__scale),
  284. Math.ceil(this.__scale),
  285. Math.ceil(this.__scale)
  286. );
  287. context.fill();
  288. context.closePath();
  289. }
  290. }
  291. // Draw grid.
  292. if (this.__gridEnabled && this.__scale > 0.5){
  293. context.strokeStyle = "#00FF00";
  294. var w = this.__surface.width * this.__scale;
  295. var h = this.__surface.height * this.__scale;
  296. var length = Math.max(this.__surface.width, this.__surface.height);
  297. for (var i=0; i < length; i += 8){
  298. var x = (i*this.__scale) + this.__offset[0];
  299. var y = (i*this.__scale) + this.__offset[1];
  300. if (i < this.__surface.width){
  301. context.beginPath();
  302. context.moveTo(x, this.__offset[1]);
  303. context.lineTo(x, this.__offset[1] + h);
  304. context.stroke();
  305. context.closePath();
  306. }
  307. if (i < this.__surface.height){
  308. context.beginPath();
  309. context.moveTo(this.__offset[0], y);
  310. context.lineTo(this.__offset[0] + w, y);
  311. context.stroke();
  312. context.closePath();
  313. }
  314. }
  315. }
  316. context.restore();
  317. return this;
  318. }
  319. }
  320. const instance = new CTRLPainter();
  321. export default instance;