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

656 行
18KB

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