A pixel art painter geared specifically at NES pixel art. Includes export for .chr binary file as well as palette and namespace data.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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