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

606 行
17KB

  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.__undos = []; // Holds Base64 snapshots of the bank.
  79. this.__redos = []; // Holds Base64 snapshots of work undone.
  80. this.__emitsEnabled = true;
  81. this.snapshot = Utils.debounce(this.snapshot.bind(this), 250);
  82. var handle_datachanged = Utils.debounce((function(side, idx){
  83. var sendEmit = false;
  84. switch(this.__AccessMode){
  85. case NESBank.ACCESSMODE_1K:
  86. if (side === Math.floor(this.__AccessOffset / 4)){
  87. if (Math.floor(idx / 64) === Math.floor(this.__AccessOffset/4))
  88. sendEmit = true;
  89. }
  90. break;
  91. case NESBank.ACCESSMODE_2K:
  92. if (side === Math.floor(this.__AccessOffset / 2)){
  93. if (Math.floor(idx / 128) === Math.floor(this.__AccessOffset/2))
  94. sendEmit = true;
  95. }
  96. break;
  97. case NESBank.ACCESSMODE_4K:
  98. if (side === this.__AccessOffset)
  99. sendEmit = true;
  100. break;
  101. case NESBank.ACCESSMODE_8K:
  102. sendEmit = true;
  103. }
  104. if (sendEmit && this.__emitsEnabled){
  105. this.emit("data_changed");
  106. }
  107. }).bind(this), 250);
  108. for (var i=0; i < 256; i++){
  109. this.__LP.push(new NESTile());
  110. this.__LP[i].listen("data_changed", handle_datachanged.bind(this, 0, i));
  111. this.__RP.push(new NESTile());
  112. this.__RP[i].listen("data_changed", handle_datachanged.bind(this, 1, i));
  113. }
  114. this.__palette = null;
  115. }
  116. get access_mode(){return this.__AccessMode;}
  117. set access_mode(m){
  118. if (!Utils.isInt(m))
  119. throw new TypeError("Access mode expected to be integer.");
  120. var oam = this.__AccessMode;
  121. switch(m){
  122. case NESBank.ACCESSMODE_8K:
  123. this.__AccessMode = NESBank.ACCESSMODE_8K;
  124. break;
  125. case NESBank.ACCESSMODE_4K:
  126. this.__AccessMode = NESBank.ACCESSMODE_4K
  127. break;
  128. case NESBank.ACCESSMODE_2K:
  129. this.__AccessMode = NESBank.ACCESSMODE_2K;
  130. break;
  131. case NESBank.ACCESSMODE_1K:
  132. this.__AccessMode = NESBank.ACCESSMODE_1K;
  133. break;
  134. default:
  135. throw new ValueError("Unknown Access Mode.");
  136. }
  137. this.__AccessOffset = AdjOffsetToNewMode(m, oam, this.__AccessOffset);
  138. if (this.__emitsEnabled)
  139. this.emit("data_changed");
  140. }
  141. get access_offset(){return this.__AccessOffset;}
  142. set access_offset(o){
  143. if (!Utils.isInt(o))
  144. throw new TypeError("Access offset expected to be integer.");
  145. switch (this.__AccessMode){
  146. case NESBank.ACCESSMODE_8K:
  147. if (o !== 0)
  148. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  149. break;
  150. case NESBank.ACCESSMODE_4K:
  151. if (o !== 0 && o !== 1)
  152. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  153. break;
  154. case NESBank.ACCESSMODE_2K:
  155. if (o < 0 || o >= 4)
  156. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  157. break;
  158. case NESBank.ACCESSMODE_1K:
  159. if (o < 0 || o >= 8)
  160. throw new RangeError("Access Offset is out of bounds based on current Access Mode.");
  161. break;
  162. }
  163. this.__AccessOffset = o;
  164. if (this.__emitsEnabled)
  165. this.emit("data_changed");
  166. }
  167. get access_offset_length(){
  168. switch(this.__AccessMode){
  169. case NESBank.ACCESSMODE_4K:
  170. return 2;
  171. case NESBank.ACCESSMODE_2K:
  172. return 4;
  173. case NESBank.ACCESSMODE_1K:
  174. return 8;
  175. }
  176. return 0;
  177. }
  178. get json(){
  179. JSON.stringify({
  180. LP: this.__LP.map(x=>x.base64),
  181. RP: this.__RP.map(x=>x.base64)
  182. });
  183. }
  184. get chr(){
  185. var buff = null;
  186. var offset = 0;
  187. switch (this.__AccessMode){
  188. case NESBank.ACCESSMODE_8K:
  189. buff = new Uint8Array(8192);
  190. this.__LP.forEach((i) => {
  191. buff.set(i.chr, offset);
  192. offset += 16;
  193. });
  194. this.__RP.forEach((i) => {
  195. buff.set(i.chr, offset);
  196. offset += 16;
  197. });
  198. break;
  199. case NESBank.ACCESSMODE_4K:
  200. buff = new Uint8Array(4096);
  201. var list = (this.__AccessOffset === 0) ? this.__LP : this.__RP;
  202. list.forEach((i) => {
  203. buff.set(i.chr, offset);
  204. offset += 16;
  205. });
  206. break;
  207. case NESBank.ACCESSMODE_2K:
  208. buff = new Uint8Array(2048);
  209. var list = (this.__AccessOffset < 2) ? this.__LP : this.__RP;
  210. var s = Math.floor(this.__AccessOffset * 0.5) * 128;
  211. var e = s + 128;
  212. for (let i=s; i < e; i++){
  213. buff.set(list[i].chr, offset);
  214. offset += 16;
  215. }
  216. break;
  217. case NESBank.ACCESSMODE_1K:
  218. buff = new Uint8Array(1024);
  219. var list = (this.__AccessOffset < 4) ? this.__LP : this.__RP;
  220. var s = Math.floor(this.__AccessOffset * 0.25) * 64;
  221. var e = s + 64;
  222. for (let i=s; i < e; i++){
  223. buff.set(list[i].chr, offset);
  224. offset += 16;
  225. }
  226. break;
  227. }
  228. return buff;
  229. }
  230. set chr(buff){
  231. if (!(buff instanceof Uint8Array))
  232. throw new TypeError("Expected Uint8Array buffer.");
  233. this.setCHR(buff);
  234. }
  235. get base64(){
  236. var b = "";
  237. var data = this.chr;
  238. for (var i = 0; i < data.length; i++) {
  239. b += String.fromCharCode(data[i]);
  240. }
  241. return window.btoa(b);
  242. }
  243. set base64(s){
  244. var b = window.atob(s);
  245. var len = b.length;
  246. if (b.length !== 8192){
  247. throw new Error("Base64 string contains invalid byte count.");
  248. }
  249. b = new Uint8Array(b.split("").map(function(c){
  250. return c.charCodeAt(0);
  251. }));
  252. this.chr = b;
  253. }
  254. get palette(){return this.__palette;}
  255. set palette(p){
  256. if (p !== null && !(p instanceof NESPalette))
  257. throw new TypeError("Expected null or NESPalette object.");
  258. if (p !== this.__palette){
  259. this.__palette = p;
  260. }
  261. }
  262. get width(){return (this.__AccessMode == NESBank.ACCESSMODE_8K) ? 256 : 128;}
  263. get height(){
  264. switch(this.__AccessMode){
  265. case NESBank.ACCESSMODE_2K:
  266. return 64;
  267. case NESBank.ACCESSMODE_1K:
  268. return 32;
  269. }
  270. return 128;
  271. }
  272. get length(){return this.width * this.height;}
  273. get undos(){return this.__undos.length;}
  274. get redos(){return this.__redos.length;}
  275. get coloridx(){
  276. return new Proxy(this, {
  277. get:function(obj, prop){
  278. var len = obj.length * 8;
  279. if (prop === "length")
  280. return len;
  281. if (!Utils.isInt(prop))
  282. throw new TypeError("Expected integer index.");
  283. prop = parseInt(prop);
  284. if (prop < 0 || prop >= len)
  285. return NESPalette.Default[4];
  286. var x = Math.floor(prop % this.width);
  287. var y = Math.floor(prop / this.width);
  288. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  289. var list = (res.side === 0) ? obj.__LP : obj.__RP;
  290. return list[res.tileidx].getPixelIndex(res.x, res.y);
  291. },
  292. set:function(obj, prop, value){
  293. if (!Utils.isInt(prop))
  294. throw new TypeError("Expected integer index.");
  295. if (!Utils.isInt(value))
  296. throw new TypeError("Color expected to be integer.");
  297. prop = parseInt(prop);
  298. value = parseInt(value);
  299. if (prop < 0 || prop >= len)
  300. throw new RangeError("Index out of bounds.");
  301. if (value < 0 || value >= 4)
  302. throw new RangeError("Color index out of bounds.");
  303. var x = Math.floor(prop % this.width);
  304. var y = Math.floor(prop / this.width);
  305. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  306. var list = (res.side === 0) ? obj.__LP : obj.__RP;
  307. list[res.tileidx].setPixelIndex(res.x, res.y, value);
  308. return true;
  309. }
  310. });
  311. }
  312. get lp(){
  313. return new Proxy(this, {
  314. get: function(obj, prop){
  315. if (prop === "length")
  316. return obj.__LP.length;
  317. if (!Utils.isInt(prop))
  318. throw new TypeError("Expected integer index.");
  319. prop = parseInt(prop);
  320. if (prop < 0 || prop >= 256)
  321. throw new RangeError("Index out of bounds.");
  322. return obj.__LP[prop];
  323. },
  324. set: function(obj, prop, value){
  325. if (!Utils.isInt(prop))
  326. throw new TypeError("Expected integer index.");
  327. if (!(value instanceof NESTile))
  328. throw new TypeError("Can only assign NESTile objects.");
  329. prop = parseInt(prop);
  330. if (prop < 0 || prop >= 256)
  331. throw new RangeError("Index out of bounds.");
  332. obj.__LP[prop].copy(value);
  333. return true;
  334. }
  335. });
  336. }
  337. get rp(){
  338. return new Proxy(this, {
  339. get: function(obj, prop){
  340. if (prop === "length")
  341. return obj.__RP.length;
  342. if (!Utils.isInt(prop))
  343. throw new TypeError("Expected integer index.");
  344. prop = parseInt(prop);
  345. if (prop < 0 || prop >= 256)
  346. throw new RangeError("Index out of bounds.");
  347. return obj.__RP[prop];
  348. },
  349. set: function(obj, prop, value){
  350. if (!Utils.isInt(prop))
  351. throw new TypeError("Expected integer index.");
  352. if (!(value instanceof NESTile))
  353. throw new TypeError("Can only assign NESTile objects.");
  354. prop = parseInt(prop);
  355. if (prop < 0 || prop >= 256)
  356. throw new RangeError("Index out of bounds.");
  357. obj.__RP[prop].copy(value);
  358. return true;
  359. }
  360. });
  361. }
  362. copy(b){
  363. if (!(b instanceof NESBank))
  364. throw new TypeError("Expected NESBank object.");
  365. for (var i=0; i < 256; i++){
  366. this.lp[i] = b.lp[i];
  367. this.rp[i] = b.rp[i];
  368. }
  369. return this;
  370. }
  371. clone(){
  372. return (new NESBank()).copy(this);
  373. }
  374. getCHR(mode, offset){
  375. this.__emitsEnabled = false;
  376. var oam = this.access_mode;
  377. var oao = this.access_offset;
  378. try{
  379. this.access_mode = mode;
  380. this.access_offset = offset;
  381. } catch (e){
  382. this.access_mode = oam;
  383. this.access_offset = oao;
  384. this.__emitsEnabled = true;
  385. throw e;
  386. }
  387. var chr = this.chr;
  388. this.access_mode = oam;
  389. this.access_offset = oao;
  390. this.__emitsEnabled = true;
  391. return chr;
  392. }
  393. setCHR(buff, offset){
  394. if (!Utils.isInt(offset))
  395. offset = -1;
  396. var idx = 0;
  397. switch(buff.length){
  398. case 8192:
  399. if (offset < 0)
  400. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_8K, this.__AccessMode, this.__AccessOffset);
  401. this.__LP.forEach((i) => {
  402. i.chr = buff.slice(idx, idx+16);
  403. idx += 16;
  404. });
  405. this.__RP.forEach((i) => {
  406. i.chr = buff.slice(idx, idx+16);
  407. idx += 16;
  408. });
  409. break;
  410. case 4096:
  411. if (offset < 0)
  412. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_4K, this.__AccessMode, this.__AccessOffset);
  413. if (offset >= 2)
  414. throw new RangeError("Offset mismatch based on Buffer length.");
  415. var list = (offset === 0) ? this.__LP : this.__RP;
  416. list.forEach((i) => {
  417. i.chr = buff.slice(idx, idx+16);
  418. idx += 16;
  419. });
  420. break;
  421. case 2048:
  422. if (offset < 0)
  423. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_2K, this.__AccessMode, this.__AccessOffset);
  424. if (offset >= 4)
  425. throw new RangeError("Offset mismatch based on Buffer length.");
  426. var list = (offset < 2) ? this.__LP : this.__RP;
  427. var s = Math.floor(offset * 0.5) * 128;
  428. var e = s + 128;
  429. for (let i=s; i < e; i++){
  430. list[i].chr = buff.slice(idx, idx+16);
  431. idx += 16;
  432. }
  433. break;
  434. case 1024:
  435. if (offset < 0)
  436. offset = AdjOffsetToNewMode(NESBank.ACCESSMODE_1K, this.__AccessMode, this.__AccessOffset);
  437. if (offset >= 8)
  438. throw new RangeError("Offset mismatch based on Buffer length.");
  439. var list = (offset < 4) ? this.__LP : this.__RP;
  440. var s = Math.floor(this.__AccessOffset * 0.25) * 64;
  441. var e = s + 64;
  442. for (let i=s; i < e; i++){
  443. list[i].chr = buff.slice(idx, idx+16);
  444. idx += 16;
  445. }
  446. break;
  447. default:
  448. throw new RangeError("Buffer length does not match any of the supported bank sizes.");
  449. }
  450. return this;
  451. }
  452. getColor(x,y){
  453. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  454. return this.__default_pi[4];
  455. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  456. var list = (res.side === 0) ? this.__LP : this.__RP;
  457. var pi = list[res.tileidx].paletteIndex + ((res.side === 0) ? 4 : 0);
  458. var ci = list[res.tileidx].getPixelIndex(res.x, res.y);
  459. if (this.__palette !== null){
  460. return this.__palette.get_palette_color(pi, ci);
  461. }
  462. return NESPalette.Default[ci];
  463. }
  464. getColorIndex(x, y){
  465. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  466. return {pi: -1, ci:-1};
  467. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  468. var list = (res.side === 0) ? this.__LP : this.__RP;
  469. return {
  470. pi: list[res.tileidx].paletteIndex,
  471. ci: list[res.tileidx].getPixelIndex(res.x, res.y)
  472. };
  473. }
  474. setColorIndex(x, y, ci, pi){
  475. if (x < 0 || x >= this.width || y < 0 || y > this.height)
  476. throw new RangeError("Coordinates out of bounds.");
  477. if (!Utils.isInt(pi))
  478. pi = -1;
  479. if (!Utils.isInt(ci))
  480. ci = 0;
  481. if (pi < 0){
  482. this.coloridx[(y*this.width)+x] = ci;
  483. } else {
  484. var res = CnvIdx(x, y, this.__AccessMode, this.__AccessOffset);
  485. var list = (res.side === 0) ? this.__LP : this.__RP;
  486. list[res.tileidx].paletteIndex = pi;
  487. list[res.tileidx].setPixelIndex(res.x, res.y, ci);
  488. }
  489. return this;
  490. }
  491. snapshot(){
  492. var snap = this.base64;
  493. if (this.__undos.length === this.__historyLength){
  494. this.__undos.pop();
  495. }
  496. this.__undos.splice(0,0,snap);
  497. return this;
  498. }
  499. undo(){
  500. if (this.__undos.length > 0){
  501. var usnap = this.__undos.splice(0, 1)[0];
  502. var rsnap = this.base64;
  503. this.base64 = usnap;
  504. if (this.__redos.length === this.__historyLength){
  505. this.__redos.pop();
  506. }
  507. this.__redos.splice(0,0,rsnap);
  508. }
  509. return this;
  510. }
  511. redo(){
  512. if (this.__redos.length > 0){
  513. var rsnap = this.__redos.splice(0,1)[0];
  514. var usnap = this.base64;
  515. this.base64 = rsnap;
  516. if (this.__undos.length === this.__historyLength){
  517. this.__undos.pop();
  518. }
  519. this.__undos.splice(0,0,usnap);
  520. }
  521. return this;
  522. }
  523. clearUndos(){
  524. this.__undos = [];
  525. return this;
  526. }
  527. clearRedos(){
  528. this.__redos = [];
  529. return this;
  530. }
  531. }
  532. NESBank.ACCESSMODE_8K = 0;
  533. NESBank.ACCESSMODE_1K = 1;
  534. NESBank.ACCESSMODE_2K = 2;
  535. NESBank.ACCESSMODE_4K = 3;