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

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