A pixel art painter geared specifically at NES pixel art. Includes export for .chr binary file as well as palette and namespace data.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

307 rindas
8.3KB

  1. import Utils from "/app/js/common/Utils.js";
  2. import ISurface from "/app/js/ifaces/ISurface.js";
  3. import NESTile from "/app/js/models/NESTile.js";
  4. import NESPalette from "/app/js/models/NESPalette.js";
  5. function LRIdx2TileIdxCo(index, lid){
  6. if (isNaN(lid) || lid < 0 || lid > 2){
  7. lid = 2;
  8. }
  9. var res = {
  10. lid: 0,
  11. index: 0,
  12. x: 0,
  13. y: 0
  14. };
  15. var w = (lid == 2) ? 256 : 128;
  16. var x = Math.floor(index % w);
  17. var y = Math.floor(index / w);
  18. if (x < 128){
  19. res.index = (Math.floor(y/8) * 16) + Math.floor(x / 8);
  20. if (lid !== 2)
  21. res.lid = lid;
  22. } else {
  23. res.index = (Math.floor(y/8) * 16) + Math.floor((x - 128) / 8);
  24. res.lid = 1;
  25. }
  26. res.x = x % 8;
  27. res.y = y % 8;
  28. return res;
  29. }
  30. export default class NESBank extends ISurface{
  31. constructor(){
  32. super();
  33. this.__LP = []; // Left Patterns (Sprites)
  34. this.__RP = []; // Right Patterns (Backgrounds)
  35. this.__AccessMode = 2; // 0 = Sprites only | 1 = BG only | 2 = Sprites and BG
  36. var handle_datachanged = Utils.debounce((function(side){
  37. if ((side == 0 && (this.__AccessMode == 0 || this.__AccessMode == 2)) ||
  38. (side == 1 && (this.__AccessMode == 1 || this.__AccessMode == 2))){
  39. this.emit("data_changed");
  40. }
  41. }).bind(this), 250);
  42. for (var i=0; i < 256; i++){
  43. this.__LP.push(new NESTile());
  44. this.__LP[i].listen("data_changed", handle_datachanged.bind(this, 0));
  45. this.__RP.push(new NESTile());
  46. this.__RP[i].listen("data_changed", handle_datachanged.bind(this, 1));
  47. }
  48. this.__palette = null;
  49. }
  50. get access_mode(){return this.__AccessMode;}
  51. set access_mode(m){
  52. if (!Utils.isInt(m))
  53. throw new TypeError("Access mode expected to be integer.");
  54. switch(m){
  55. case NESBank.ACCESSMODE_SPRITE:
  56. this.__AccessMode = NESBank.ACCESSMODE_SPRITE;
  57. this.emit("data_changed");
  58. break;
  59. case NESBank.ACCESSMODE_BACKGROUND:
  60. this.__AccessMode = NESBank.ACCESSMODE_BACKGROUND;
  61. this.emit("data_changed");
  62. break;
  63. case NESBank.ACCESSMODE_FULL:
  64. this.__AccessMode = NESBank.ACCESSMODE_FULL;
  65. this.emit("data_changed");
  66. break;
  67. }
  68. }
  69. get json(){
  70. JSON.stringify({
  71. LP: this.__LP.map(x=>x.base64),
  72. RP: this.__RP.map(x=>x.base64)
  73. });
  74. }
  75. get chr(){
  76. var buff = new Uint8Array(8192);
  77. var offset = 0;
  78. this.__LP.forEach(function(i){
  79. buff.set(i.chr, offset);
  80. offset += 16;
  81. });
  82. this.__RP.forEach(function(i){
  83. buff.set(i.chr, offset);
  84. offset += 16;
  85. });
  86. return buff;
  87. }
  88. set chr(buff){
  89. if (!(buff instanceof Uint8Array))
  90. throw new TypeError("Expected Uint8Array buffer.");
  91. if (buff.length !== 8192)
  92. throw new RangeError("Data buffer has invalid byte length.");
  93. var offset = 0;
  94. this.__LP.forEach((i) => {
  95. i.chr = buff.slice(offset, offset+15);
  96. offset += 16;
  97. });
  98. this.__RP.forEach((i) => {
  99. i.chr = buff.slice(offset, offset+15);
  100. offset += 16;
  101. });
  102. }
  103. get base64(){
  104. var b = "";
  105. var data = this.chr;
  106. for (var i = 0; i < data.length; i++) {
  107. b += String.fromCharCode(data[i]);
  108. }
  109. return window.btoa(b);
  110. }
  111. set base64(s){
  112. var b = window.atob(s);
  113. var len = b.length;
  114. if (b.length !== 8192){
  115. throw new Error("Base64 string contains invalid byte count.");
  116. }
  117. this.chr = b;
  118. }
  119. get palette(){return this.__palette;}
  120. set palette(p){
  121. if (p !== null && !(p instanceof NESPalette))
  122. throw new TypeError("Expected null or NESPalette object.");
  123. if (p !== this.__palette){
  124. this.__palette = p;
  125. }
  126. }
  127. get width(){return (this.__AccessMode == NESBank.ACCESSMODE_FULL) ? 256 : 128;}
  128. get height(){return 128;}
  129. get length(){return this.width * this.height;}
  130. get coloridx(){
  131. return new Proxy(this, {
  132. get:function(obj, prop){
  133. var len = obj.length * 8;
  134. if (prop === "length")
  135. return len;
  136. if (!Utils.isInt(prop))
  137. throw new TypeError("Expected integer index.");
  138. prop = parseInt(prop);
  139. if (prop < 0 || prop >= len)
  140. return NESPalette.Default[4];
  141. var res = LRIdx2TileIdxCo(prop, this.__AccessMode);
  142. var list = (res.lid === 0) ? obj.__LP : obj.__RP;
  143. return list[res.index].getPixelIndex(res.x, res.y);
  144. },
  145. set:function(obj, prop, value){
  146. if (!Utils.isInt(prop))
  147. throw new TypeError("Expected integer index.");
  148. if (!Utils.isInt(value))
  149. throw new TypeError("Color expected to be integer.");
  150. prop = parseInt(prop);
  151. value = parseInt(value);
  152. if (prop < 0 || prop >= len)
  153. throw new RangeError("Index out of bounds.");
  154. if (value < 0 || value >= 4)
  155. throw new RangeError("Color index out of bounds.");
  156. var res = LRIdx2TileIdxCo(prop, this.__AccessMode);
  157. var list = (res.lid === 0) ? obj.__LP : obj.__RP;
  158. list[res.index].setPixelIndex(res.x, res.y, value);
  159. return true;
  160. }
  161. });
  162. }
  163. get lp(){
  164. return new Proxy(this, {
  165. get: function(obj, prop){
  166. if (prop === "length")
  167. return obj.__LP.length;
  168. if (!Utils.isInt(prop))
  169. throw new TypeError("Expected integer index.");
  170. prop = parseInt(prop);
  171. if (prop < 0 || prop >= 256)
  172. throw new RangeError("Index out of bounds.");
  173. return obj.__LP[prop];
  174. },
  175. set: function(obj, prop, value){
  176. if (!Utils.isInt(prop))
  177. throw new TypeError("Expected integer index.");
  178. if (!(value instanceof NESTile))
  179. throw new TypeError("Can only assign NESTile objects.");
  180. prop = parseInt(prop);
  181. if (prop < 0 || prop >= 256)
  182. throw new RangeError("Index out of bounds.");
  183. obj.__LP[prop].copy(value);
  184. return true;
  185. }
  186. });
  187. }
  188. get rp(){
  189. return new Proxy(this, {
  190. get: function(obj, prop){
  191. if (prop === "length")
  192. return obj.__RP.length;
  193. if (!Utils.isInt(prop))
  194. throw new TypeError("Expected integer index.");
  195. prop = parseInt(prop);
  196. if (prop < 0 || prop >= 256)
  197. throw new RangeError("Index out of bounds.");
  198. return obj.__RP[prop];
  199. },
  200. set: function(obj, prop, value){
  201. if (!Utils.isInt(prop))
  202. throw new TypeError("Expected integer index.");
  203. if (!(value instanceof NESTile))
  204. throw new TypeError("Can only assign NESTile objects.");
  205. prop = parseInt(prop);
  206. if (prop < 0 || prop >= 256)
  207. throw new RangeError("Index out of bounds.");
  208. obj.__RP[prop].copy(value);
  209. return true;
  210. }
  211. });
  212. }
  213. copy(b){
  214. if (!(b instanceof NESBank))
  215. throw new TypeError("Expected NESBank object.");
  216. for (var i=0; i < 256; i++){
  217. this.lp[i] = b.lp[i];
  218. this.rp[i] = b.rp[i];
  219. }
  220. return this;
  221. }
  222. clone(){
  223. return (new NESBank()).copy(this);
  224. }
  225. getColor(x,y){
  226. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  227. return this.__default_pi[4];
  228. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  229. var list = (res.lid === 0) ? this.__LP : this.__RP;
  230. var pi = list[res.index].paletteIndex;
  231. var ci = list[res.index].getPixelIndex(res.x, res.y);
  232. if (this.__palette !== null){
  233. return this.__palette.get_palette_color(pi, ci);
  234. }
  235. return NESPalette.Default[ci];
  236. }
  237. getColorIndex(x, y){
  238. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  239. return {pi: -1, ci:-1};
  240. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  241. var list = (res.lid === 0) ? this.__LP : this.__RP;
  242. return {
  243. pi: list[res.index].paletteIndex,
  244. ci: list[res.index].getPixelIndex(res.x, res.y)
  245. };
  246. }
  247. setColorIndex(x, y, ci, pi){
  248. if (x < 0 || x >= this.width || y < 0 || y > this.height)
  249. throw new RangeError("Coordinates out of bounds.");
  250. if (!Utils.isInt(pi))
  251. pi = -1;
  252. if (!Utils.isInt(ci))
  253. ci = 0;
  254. if (pi < 0){
  255. this.coloridx[(y*this.width)+x] = ci;
  256. } else {
  257. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  258. var list = (res.lid === 0) ? this.__LP : this.__RP;
  259. list[res.index].paletteIndex = pi;
  260. list[res.index].setPixelIndex(res.x, res.y, ci);
  261. }
  262. return this;
  263. }
  264. }
  265. NESBank.ACCESSMODE_SPRITE = 0;
  266. NESBank.ACCESSMODE_BACKGROUND = 1;
  267. NESBank.ACCESSMODE_FULL = 2;