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文字以内のものにしてください。

313 行
8.0KB

  1. import Utils from "/app/js/common/Utils.js";
  2. import ISurface from "/app/js/ifaces/ISurface.js";
  3. import NESBank from "/app/js/models/NESBank.js";
  4. import NESPalette from "/app/js/models/NESPalette.js";
  5. function NumToHex(n){
  6. var h = n.toString(16);
  7. if (h.length %2)
  8. h = '0' + h;
  9. return '$' + h;
  10. }
  11. function CompileAttribs(attrib){
  12. return (attrib[3] << 6) | (attrib[2] << 4) | (attrib[1] << 2) | (attrib[0] << 0)
  13. }
  14. function DecompileAttribs(v){
  15. return [
  16. v & 0x00000011,
  17. (v & 0x00001100) >> 2,
  18. (v & 0x00110000) >> 4,
  19. (v & 0x11000000) >> 6
  20. ];
  21. }
  22. export default class NESNameTable extends ISurface{
  23. constructor(){
  24. super();
  25. this.__bank = null;
  26. this.__palette = null;
  27. this.__tiles = [];
  28. this.__attribs = [];
  29. this.__undos = [];
  30. this.__redos = [];
  31. for (let i=0; i < 960; i++)
  32. this.__tiles[i] = 0;
  33. for (let i=0; i < 64; i++)
  34. this.__attribs[i] = [0,0,0,0];
  35. }
  36. get base64(){
  37. var b = "";
  38. for (let i = 0; i < this.__tiles.length; i++)
  39. b += String.fromCharCode(this.__tiles[i]);
  40. for (let i = 0; i < this.__attribs.length; i++)
  41. b += String.fromCharCode(CompileAttribs(this.__attribs[i]));
  42. return window.btoa(b);
  43. }
  44. set base64(s){
  45. var b = window.atob(s);
  46. var len = b.length;
  47. if (b.length !== 1024){
  48. throw new Error("Base64 string contains invalid byte count.");
  49. }
  50. b = new Uint8Array(b.split("").map(function(c){
  51. return c.charCodeAt(0);
  52. }));
  53. for (let i=0; i < b.length; i++){
  54. if (i < 960){
  55. this.__tiles[i] = b[i];
  56. } else {
  57. this.__attribs[i-960] = DecompileAttribs(b[i]);
  58. }
  59. }
  60. this.emit("data_changed");
  61. }
  62. get bank(){return this.__bank;}
  63. set bank(b){
  64. if (b !== null && !(b instanceof NESBank))
  65. throw new TypeError("Expected a NESBank object.");
  66. this.__bank = b;
  67. this.emit("data_changed");
  68. }
  69. get palette(){return this.__palette;}
  70. set palette(p){
  71. if (p !== null && !(p instanceof NESPalette))
  72. throw new TypeError("Expected a NESPalette object.");
  73. this.__palette = p;
  74. this.emit("data_changed");
  75. }
  76. get width(){return 256;}
  77. get height(){return 240;}
  78. get length(){return this.width * this.height;}
  79. get undos(){return this.__undos.length;}
  80. get redos(){return this.__redos.length;}
  81. copy(b){
  82. this.bank = b.bank;
  83. this.palette = b.palette;
  84. for (let i=0; i < 960; i++)
  85. this.__tiles[i] = b.__tiles[i];
  86. for (let i=0; i < 64; i++){
  87. this.__attribs[i] = [
  88. b.__attribs[i][0],
  89. b.__attribs[i][1],
  90. b.__attribs[i][2],
  91. b.__attribs[i][3]
  92. ];
  93. }
  94. this.emit("data_changed");
  95. return this;
  96. }
  97. clone(){
  98. return (new NESNameTable()).clone(this);
  99. }
  100. snapshot(){
  101. if (this.__redos.length > 0) // Remove the redo history. We're adding a new snapshot.
  102. this.__redos = [];
  103. var snap = this.base64;
  104. if (this.__undos.length === this.__historyLength){
  105. this.__undos.pop();
  106. }
  107. this.__undos.splice(0,0,snap);
  108. return this;
  109. }
  110. undo(){
  111. if (this.__undos.length > 0){
  112. var usnap = this.__undos.splice(0, 1)[0];
  113. var rsnap = this.base64;
  114. this.base64 = usnap;
  115. if (this.__redos.length === this.__historyLength){
  116. this.__redos.pop();
  117. }
  118. this.__redos.splice(0,0,rsnap);
  119. }
  120. return this;
  121. }
  122. redo(){
  123. if (this.__redos.length > 0){
  124. var rsnap = this.__redos.splice(0,1)[0];
  125. var usnap = this.base64;
  126. this.base64 = rsnap;
  127. if (this.__undos.length === this.__historyLength){
  128. this.__undos.pop();
  129. }
  130. this.__undos.splice(0,0,usnap);
  131. }
  132. return this;
  133. }
  134. clearUndos(){
  135. this.__undos = [];
  136. return this;
  137. }
  138. clearRedos(){
  139. this.__redos = [];
  140. return this;
  141. }
  142. getColor(x, y){
  143. var pal = {pi:-1, ci:-1};
  144. try {
  145. pal = this.getColorIndex(x, y);
  146. } catch (e) {throw e;}
  147. if (this.__palette !== null && pal.pi >= 0 && pal.ci >= 0) {
  148. return this.__palette.get_palette_color(pal.pi, pal.ci);
  149. } else if (pal.ci >= 0){
  150. return NESPalette.Default(pal.ci);
  151. }
  152. return NESPalette.Default(4);
  153. }
  154. getColorIndex(x, y){
  155. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  156. throw new RangeError("Coordinates are out of bounds.");
  157. var pi = -1;
  158. var ci = -1;
  159. if (this.__bank !== null){
  160. var _x = Math.floor(x % 8);
  161. var _y = Math.floor(y % 8);
  162. var tileX = Math.floor(x / 8);
  163. var tileY = Math.floor(y / 8);
  164. ci = this.__bank.rp[this.__tiles[(tileY * 32) + tileX]].getPixelIndex(_x, _y);
  165. pi = this._PaletteFromCoords(x, y);
  166. }
  167. return {pi:pi, ci:ci};
  168. }
  169. setColorIndex(x, y, ci, pi){
  170. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  171. throw new RangeError("Coordinates are out of bounds.");
  172. if (pi < 0 || pi >= 4)
  173. throw new RangeError("Palette index is out of bounds.");
  174. // NOTE: This method (setColorIndex) is called by CTRLPainter, which doesn't know about painting tile
  175. // indicies... therefore, CTRLPainter will still call this method for NESNameTable surfaces, but all it
  176. // will do is change palette indicies.
  177. // To paint the actual tile index, however, we'll use this emit that will be watched by the CTRLNameTable class
  178. // and call this class's setTileIndex() method for tile painting.
  179. // YAY to cheating!!
  180. this.emit("paint_nametable", x, y);
  181. // Then business as usual!
  182. var bp = this._GetAttribBlockPalette(x,y);
  183. if (this.__attribs[bp.bindex][bp.pindex] !== pi){
  184. this.__attribs[bp.bindex][bp.pindex] = pi;
  185. this.emit("data_changed");
  186. }
  187. return this;
  188. }
  189. setTileIndex(x, y, ti){
  190. if (ti < 0 || ti >= 256)
  191. throw new RangeError("Tile index is out of bounds.");
  192. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  193. throw new RangeError("Coordinates are out of bounds.");
  194. var _x = Math.floor(x / 8);
  195. var _y = Math.floor(y / 8);
  196. var tindex = (_y * 32) + _x;
  197. if (this.__tiles[tindex] !== ti){
  198. this.__tiles[tindex] = ti;
  199. this.emit("data_changed");
  200. }
  201. }
  202. eq(nt){
  203. return (nt instanceof NESNameTable) ? (this.base64 === nt.base64) : false;
  204. }
  205. /**
  206. * Generates a small 6502 assembly block string containing the current nametable data.
  207. * @param {string} [ntname="NameTableData"] The label name under which to store the data.
  208. * @returns {string}
  209. */
  210. nametable_asm(ntname="NameTableData"){
  211. var s = ntname + ":";
  212. for (let i=0; i < this.__tiles.length; i++){
  213. if (i % 32 === 0)
  214. s += "\n\t.db";
  215. s += " " + NumToHex(this.__tiles[i]);
  216. }
  217. return s;
  218. }
  219. /**
  220. * Generates a small 6502 assembly block string containing the current attribute table data.
  221. * @param {string} [atname="AttribTableData"] The label name under which to store the data.
  222. * @returns {string}
  223. */
  224. attribtable_asm(atname="AttribTableData"){
  225. var s = atname + ":";
  226. for (let i=0; i < this.__attribs.length; i++){
  227. if (i % 32 === 0)
  228. s += "\n\t.db";
  229. s += " " + NumToHex(CompileAttribs(this.__attribs[i]));
  230. }
  231. return s;
  232. }
  233. /**
  234. * Generates a small 6502 assembly block string containing the current name and attribute table data.
  235. * @param {string} [ntname="NameTableData"] The label name under which to store the Nametable data.
  236. * @param {string} [atname="AttribTableData"] the label name under which to store the Attribute table data.
  237. * @returns {string}
  238. */
  239. to_asm(ntname="NameTableData", atname="AttribTableData"){
  240. return this.nametable_asm(ntname) + "\n\n" + this.attribtable_asm(atname);
  241. }
  242. _GetAttribBlockPalette(x,y){
  243. var bp = {bindex:0, pindex:0};
  244. var blockX = Math.floor(x / 32);
  245. var blockY = Math.floor(y / 32);
  246. bp.bindex = (blockY * 8) + blockX;
  247. var palX = Math.floor(x % 32);
  248. var palY = Math.floor(y % 32);
  249. bp.pindex = ((palX < 16) ? 0 : 1) + ((palY >= 16) ? 2 : 0);
  250. return bp;
  251. }
  252. _PaletteFromCoords(x,y){
  253. var bp = this._GetAttribBlockPalette(x,y);
  254. return this.__attribs[bp.bindex][bp.pindex];
  255. }
  256. }