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

572 行
16KB

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