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

315 行
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. }