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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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 CnvIdx(x, y, am, off){
  6. var res = {
  7. side: 0,
  8. tileidx: 0,
  9. x: 0,
  10. y: 0
  11. }
  12. switch(am){
  13. case NESBank.ACCESSMODE_8K:
  14. res.side = (x >= 128) ? 1 : 0;
  15. x -= (res.side === 1) ? 128 : 0;
  16. res.tileidx = (Math.floor(y/8) * 16) + Math.floor(x / 8);
  17. break;
  18. case NESBank.ACCESSMODE_4K:
  19. res.side = off;
  20. res.tileidx = (Math.floor(y/8) * 16) + Math.floor(x / 8);
  21. break;
  22. case NESBank.ACCESSMODE_2K:
  23. res.side = Math.floor(off * 0.5);
  24. off -= (off > 1) ? 2 : 0;
  25. res.tileidx = (off*128) + ((Math.floor(y/8) * 16) + Math.floor(x / 8));
  26. break;
  27. case NESBank.ACCESSMODE_1K:
  28. res.side = Math.floor(off * 0.25);
  29. off -= (off > 3) ? 4 : 0;
  30. res.tileidx = (off * 64) + ((Math.floor(y/8) * 16) + Math.floor(x / 8));
  31. break;
  32. }
  33. res.x = x%8;
  34. res.y = y%8;
  35. return res;
  36. }
  37. function AdjOffsetToNewMode(nmode, omode, ooff){
  38. // NOTE: 8K never shows up because it will ALWAYS return an offset of 0, so it's easier to just let it all fall through
  39. // to the default return value.
  40. switch(nmode){
  41. case NESBank.ACCESSMODE_4K:
  42. if (ooff > 1){
  43. switch(omode){
  44. case NESBank.ACCESSMODE_2K:
  45. return Math.floor(ooff * 0.5);
  46. case NESBank.ACCESSMODE_1K:
  47. return Math.floor(ooff * 0.25);
  48. }
  49. }
  50. return ooff;
  51. case NESBank.ACCESSMODE_2K:
  52. switch(omode){
  53. case NESBank.ACCESSMODE_4K:
  54. return ooff * 2;
  55. case NESBank.ACCESSMODE_1K:
  56. return Math.floor(ooff * 0.5);
  57. }
  58. break;
  59. case NESBank.ACCESSMODE_1K:
  60. switch(omode){
  61. case NESBank.ACCESSMODE_4K:
  62. return ooff * 4;
  63. case NESBank.ACCESSMODE_2K:
  64. return ooff * 2;
  65. }
  66. break;
  67. }
  68. return 0;
  69. }
  70. export default class NESBank extends ISurface{
  71. constructor(){
  72. super();
  73. this.__LP = []; // Left Patterns (Sprites)
  74. this.__RP = []; // Right Patterns (Backgrounds)
  75. this.__View = [];
  76. this.__AccessMode = NESBank.ACCESSMODE_8K;
  77. this.__AccessOffset = 0;
  78. this.__emitsEnabled = true;
  79. var handle_datachanged = Utils.debounce((function(side, idx){
  80. var sendEmit = false;
  81. switch(this.__AccessMode){
  82. case NESBank.ACCESSMODE_1K:
  83. if (side === Math.floor(this.__AccessOffset / 4)){
  84. if (Math.floor(idx / 64) === Math.floor(this.__AccessOffset/4))
  85. sendEmit = true;
  86. }
  87. break;
  88. case NESBank.ACCESSMODE_2K:
  89. if (side === Math.floor(this.__AccessOffset / 2)){
  90. if (Math.floor(idx / 128) === Math.floor(this.__AccessOffset/2))
  91. sendEmit = true;
  92. }
  93. break;
  94. case NESBank.ACCESSMODE_4K:
  95. if (side === this.__AccessOffset)
  96. sendEmit = true;
  97. break;
  98. case NESBank.ACCESSMODE_8K:
  99. sendEmit = true;
  100. }
  101. if (sendEmit && this.__emitsEnabled){
  102. this.emit("data_changed");
  103. }
  104. }).bind(this), 250);
  105. for (var i=0; i < 256; i++){
  106. this.__LP.push(new NESTile());
  107. this.__LP[i].listen("data_changed", handle_datachanged.bind(this, 0, i));
  108. this.__RP.push(new NESTile());
  109. this.__RP[i].listen("data_changed", handle_datachanged.bind(this, 1, i));
  110. }
  111. this.__palette = null;
  112. }
  113. get access_mode(){return this.__AccessMode;}
  114. set access_mode(m){
  115. if (!Utils.isInt(m))
  116. throw new TypeError("Access mode expected to be integer.");
  117. var oam = this.__AccessMode;
  118. switch(m){
  119. case NESBank.ACCESSMODE_8K:
  120. this.__AccessMode = NESBank.ACCESSMODE_8K;
  121. break;
  122. case NESBank.ACCESSMODE_4K:
  123. this.__AccessMode = NESBank.ACCESSMODE_4K
  124. break;
  125. case NESBank.ACCESSMODE_2K:
  126. this.__AccessMode = NESBank.ACCESSMODE_2K;
  127. break;
  128. case NESBank.ACCESSMODE_1K:
  129. this.__AccessMode = NESBank.ACCESSMODE_1K;
  130. break;
  131. default:
  132. throw new ValueError("Unknown Access Mode.");
  133. }
  134. this.__AccessOffset = AdjOffsetToNewMode(m, oam, this.__AccessOffset);
  135. if (this.__emitsEnabled)
  136. this.emit("data_changed");
  137. }
  138. get access_offset(){return this.__AccessOffset;}
  139. set access_offset(o){
  140. if (!Utils.isInt(o))
  141. throw new TypeError("Access offset expected to be integer.");
  142. switch (this.__AccessMode){
  143. case NESBank.ACCESSMODE_8K:
  144. if (o !== 0)
  145. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  146. break;
  147. case NESBank.ACCESSMODE_4K:
  148. if (o !== 0 && o !== 1)
  149. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  150. break;
  151. case NESBank.ACCESSMODE_2K:
  152. if (o < 0 || o >= 4)
  153. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  154. break;
  155. case NESBank.ACCESSMODE_1K:
  156. if (o < 0 || o >= 8)
  157. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  158. break;
  159. }
  160. this.__AccessOffset = o;
  161. if (this.__emitsEnabled)
  162. this.emit("data_changed");
  163. }
  164. get access_offset_length(){
  165. switch(this.__AccessMode){
  166. case NESBank.ACCESSMODE_4K:
  167. return 2;
  168. case NESBank.ACCESSMODE_2K:
  169. return 4;
  170. case NESBank.ACCESSMODE_1K:
  171. return 8;
  172. }
  173. return 0;
  174. }
  175. get json(){
  176. JSON.stringify({
  177. LP: this.__LP.map(x=>x.base64),
  178. RP: this.__RP.map(x=>x.base64)
  179. });
  180. }
  181. get chr(){
  182. var buff = null;
  183. var offset = 0;
  184. switch (this.__AccessMode){
  185. case NESBank.ACCESSMODE_8K:
  186. buff = new Uint8Array(8192);
  187. this.__LP.forEach((i) => {
  188. buff.set(i.chr, offset);
  189. offset += 16;
  190. });
  191. this.__RP.forEach((i) => {
  192. buff.set(i.chr, offset);
  193. offset += 16;
  194. });
  195. break;
  196. case NESBank.ACCESSMODE_4K:
  197. buff = new Uint8Array(4096);
  198. var list = (this.__AccessOffset === 0) ? this.__LP : this.__RP;
  199. list.forEach((i) => {
  200. buff.set(i.chr, offset);
  201. offset += 16;
  202. });
  203. break;
  204. case NESBank.ACCESSMODE_2K:
  205. buff = new Uint8Array(2048);
  206. var list = (this.__AccessOffset < 2) ? this.__LP : this.__RP;
  207. var s = Math.floor(this.__AccessOffset * 0.5) * 128;
  208. var e = s + 128;
  209. for (let i=s; i < e; i++){
  210. buff.set(list[i].chr, offset);
  211. offset += 16;
  212. }
  213. break;
  214. case NESBank.ACCESSMODE_1K:
  215. buff = new Uint8Array(1024);
  216. var list = (this.__AccessOffset < 4) ? this.__LP : this.__RP;
  217. var s = Math.floor(this.__AccessOffset * 0.25) * 64;
  218. var e = s + 64;
  219. for (let i=s; i < e; i++){
  220. buff.set(list[i].chr, offset);
  221. offset += 16;
  222. }
  223. break;
  224. }
  225. return buff;
  226. }
  227. set chr(buff){
  228. if (!(buff instanceof Uint8Array))
  229. throw new TypeError("Expected Uint8Array buffer.");
  230. this.setCHR(buff);
  231. }
  232. get base64(){
  233. var b = "";
  234. var data = this.chr;
  235. for (var i = 0; i < data.length; i++) {
  236. b += String.fromCharCode(data[i]);
  237. }
  238. return window.btoa(b);
  239. }
  240. set base64(s){
  241. var b = window.atob(s);
  242. var len = b.length;
  243. if (b.length !== 8192){
  244. throw new Error("Base64 string contains invalid byte count.");
  245. }
  246. this.chr = b;
  247. }
  248. get palette(){return this.__palette;}
  249. set palette(p){
  250. if (p !== null && !(p instanceof NESPalette))
  251. throw new TypeError("Expected null or NESPalette object.");
  252. if (p !== this.__palette){
  253. this.__palette = p;
  254. }
  255. }
  256. get width(){return (this.__AccessMode == NESBank.ACCESSMODE_8K) ? 256 : 128;}
  257. get height(){
  258. switch(this.__AccessMode){
  259. case NESBank.ACCESSMODE_2K:
  260. return 64;
  261. case NESBank.ACCESSMODE_1K:
  262. return 32;
  263. }
  264. return 128;
  265. }
  266. get length(){return this.width * this.height;}
  267. get coloridx(){
  268. return new Proxy(this, {
  269. get:function(obj, prop){
  270. var len = obj.length * 8;
  271. if (prop === "length")
  272. return len;
  273. if (!Utils.isInt(prop))
  274. throw new TypeError("Expected integer index.");
  275. prop = parseInt(prop);
  276. if (prop < 0 || prop >= len)
  277. return NESPalette.Default[4];
  278. var x = Math.floor(prop % this.width);
  279. var y = Math.floor(prop / this.width);
  280. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  281. var list = (res.side === 0) ? obj.__LP : obj.__RP;
  282. return list[res.tileidx].getPixelIndex(res.x, res.y);
  283. },
  284. set:function(obj, prop, value){
  285. if (!Utils.isInt(prop))
  286. throw new TypeError("Expected integer index.");
  287. if (!Utils.isInt(value))
  288. throw new TypeError("Color expected to be integer.");
  289. prop = parseInt(prop);
  290. value = parseInt(value);
  291. if (prop < 0 || prop >= len)
  292. throw new RangeError("Index out of bounds.");
  293. if (value < 0 || value >= 4)
  294. throw new RangeError("Color index out of bounds.");
  295. var x = Math.floor(prop % this.width);
  296. var y = Math.floor(prop / this.width);
  297. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  298. var list = (res.side === 0) ? obj.__LP : obj.__RP;
  299. list[res.tileidx].setPixelIndex(res.x, res.y, value);
  300. return true;
  301. }
  302. });
  303. }
  304. get lp(){
  305. return new Proxy(this, {
  306. get: function(obj, prop){
  307. if (prop === "length")
  308. return obj.__LP.length;
  309. if (!Utils.isInt(prop))
  310. throw new TypeError("Expected integer index.");
  311. prop = parseInt(prop);
  312. if (prop < 0 || prop >= 256)
  313. throw new RangeError("Index out of bounds.");
  314. return obj.__LP[prop];
  315. },
  316. set: function(obj, prop, value){
  317. if (!Utils.isInt(prop))
  318. throw new TypeError("Expected integer index.");
  319. if (!(value instanceof NESTile))
  320. throw new TypeError("Can only assign NESTile objects.");
  321. prop = parseInt(prop);
  322. if (prop < 0 || prop >= 256)
  323. throw new RangeError("Index out of bounds.");
  324. obj.__LP[prop].copy(value);
  325. return true;
  326. }
  327. });
  328. }
  329. get rp(){
  330. return new Proxy(this, {
  331. get: function(obj, prop){
  332. if (prop === "length")
  333. return obj.__RP.length;
  334. if (!Utils.isInt(prop))
  335. throw new TypeError("Expected integer index.");
  336. prop = parseInt(prop);
  337. if (prop < 0 || prop >= 256)
  338. throw new RangeError("Index out of bounds.");
  339. return obj.__RP[prop];
  340. },
  341. set: function(obj, prop, value){
  342. if (!Utils.isInt(prop))
  343. throw new TypeError("Expected integer index.");
  344. if (!(value instanceof NESTile))
  345. throw new TypeError("Can only assign NESTile objects.");
  346. prop = parseInt(prop);
  347. if (prop < 0 || prop >= 256)
  348. throw new RangeError("Index out of bounds.");
  349. obj.__RP[prop].copy(value);
  350. return true;
  351. }
  352. });
  353. }
  354. copy(b){
  355. if (!(b instanceof NESBank))
  356. throw new TypeError("Expected NESBank object.");
  357. for (var i=0; i < 256; i++){
  358. this.lp[i] = b.lp[i];
  359. this.rp[i] = b.rp[i];
  360. }
  361. return this;
  362. }
  363. clone(){
  364. return (new NESBank()).copy(this);
  365. }
  366. getCHR(mode, offset){
  367. this.__emitsEnabled = false;
  368. var oam = this.access_mode;
  369. var oao = this.access_offset;
  370. try{
  371. this.access_mode = mode;
  372. this.access_offset = offset;
  373. } catch (e){
  374. this.access_mode = oam;
  375. this.access_offset = oao;
  376. this.__emitsEnabled = true;
  377. throw e;
  378. }
  379. var chr = this.chr;
  380. this.access_mode = oam;
  381. this.access_offset = oao;
  382. this.__emitsEnabled = true;
  383. return chr;
  384. }
  385. setCHR(buff, offset){
  386. if (!Utils.isInt(offset))
  387. offset = -1;
  388. var idx = 0;
  389. switch(buff.length){
  390. case 8192:
  391. if (offset < 0)
  392. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_8K, this.__AccessMode, this.__AccessOffset);
  393. this.__LP.forEach((i) => {
  394. i.chr = buff.slice(idx, idx+16);
  395. idx += 16;
  396. });
  397. this.__RP.forEach((i) => {
  398. i.chr = buff.slice(idx, idx+16);
  399. idx += 16;
  400. });
  401. break;
  402. case 4096:
  403. if (offset < 0)
  404. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_4K, this.__AccessMode, this.__AccessOffset);
  405. if (offset >= 2)
  406. throw new RangeError("Offset mismatch based on Buffer length.");
  407. var list = (offset === 0) ? this.__LP : this.__RP;
  408. list.forEach((i) => {
  409. i.chr = buff.slice(idx, idx+16);
  410. idx += 16;
  411. });
  412. break;
  413. case 2048:
  414. if (offset < 0)
  415. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_2K, this.__AccessMode, this.__AccessOffset);
  416. if (offset >= 4)
  417. throw new RangeError("Offset mismatch based on Buffer length.");
  418. var list = (offset < 2) ? this.__LP : this.__RP;
  419. var s = Math.floor(offset * 0.5) * 128;
  420. var e = s + 128;
  421. for (let i=s; i < e; i++){
  422. list[i].chr = buff.slice(idx, idx+16);
  423. idx += 16;
  424. }
  425. break;
  426. case 1024:
  427. if (offset < 0)
  428. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_1K, this.__AccessMode, this.__AccessOffset);
  429. if (offset >= 8)
  430. throw new RangeError("Offset mismatch based on Buffer length.");
  431. var list = (offset < 4) ? this.__LP : this.__RP;
  432. var s = Math.floor(this.__AccessOffset * 0.25) * 64;
  433. var e = s + 64;
  434. for (let i=s; i < e; i++){
  435. list[i].chr = buff.slice(idx, idx+16);
  436. idx += 16;
  437. }
  438. break;
  439. default:
  440. throw new RangeError("Buffer length does not match any of the supported bank sizes.");
  441. }
  442. return this;
  443. }
  444. getColor(x,y){
  445. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  446. return this.__default_pi[4];
  447. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  448. var list = (res.side === 0) ? this.__LP : this.__RP;
  449. var pi = list[res.tileidx].paletteIndex + ((res.side === 0) ? 4 : 0);
  450. var ci = list[res.tileidx].getPixelIndex(res.x, res.y);
  451. if (this.__palette !== null){
  452. return this.__palette.get_palette_color(pi, ci);
  453. }
  454. return NESPalette.Default[ci];
  455. }
  456. getColorIndex(x, y){
  457. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  458. return {pi: -1, ci:-1};
  459. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  460. var list = (res.side === 0) ? this.__LP : this.__RP;
  461. return {
  462. pi: list[res.tileidx].paletteIndex,
  463. ci: list[res.tileidx].getPixelIndex(res.x, res.y)
  464. };
  465. }
  466. setColorIndex(x, y, ci, pi){
  467. if (x < 0 || x >= this.width || y < 0 || y > this.height)
  468. throw new RangeError("Coordinates out of bounds.");
  469. if (!Utils.isInt(pi))
  470. pi = -1;
  471. if (!Utils.isInt(ci))
  472. ci = 0;
  473. if (pi < 0){
  474. this.coloridx[(y*this.width)+x] = ci;
  475. } else {
  476. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  477. var list = (res.side === 0) ? this.__LP : this.__RP;
  478. list[res.tileidx].paletteIndex = pi;
  479. list[res.tileidx].setPixelIndex(res.x, res.y, ci);
  480. }
  481. return this;
  482. }
  483. }
  484. NESBank.ACCESSMODE_8K = 0;
  485. NESBank.ACCESSMODE_1K = 1;
  486. NESBank.ACCESSMODE_2K = 2;
  487. NESBank.ACCESSMODE_4K = 3;