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

307 行
8.4KB

  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 + ((res.lid === 0) ? 4 : 0);
  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;