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

308 行
8.4KB

  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 LRIdx2TileIdxCo(index, lid){
  6. if (isNaN(lid) || lid < 0 || lid > 2){
  7. lid = 2;
  8. }
  9. var res = {
  10. lid: 0,
  11. index: 0,
  12. x: 0,
  13. y: 0
  14. };
  15. var w = (lid == 2) ? 256 : 128;
  16. var x = Math.floor(index % w);
  17. var y = Math.floor(index / w);
  18. if (x < 128){
  19. res.index = (Math.floor(y/8) * 16) + Math.floor(x / 8);
  20. if (lid !== 2)
  21. res.lid = lid;
  22. } else {
  23. res.index = (Math.floor(y/8) * 16) + Math.floor((x - 128) / 8);
  24. res.lid = 1;
  25. }
  26. res.x = x % 8;
  27. res.y = y % 8;
  28. return res;
  29. }
  30. export default class NESBank extends ISurface{
  31. constructor(){
  32. super();
  33. this.__LP = []; // Left Patterns (Sprites)
  34. this.__RP = []; // Right Patterns (Backgrounds)
  35. this.__View = [];
  36. this.__AccessMode = NESBank.ACCESSMODE_8K;
  37. this.__AccessOffset = 0;
  38. var handle_datachanged = Utils.debounce((function(side){
  39. if ((side == 0 && (this.__AccessMode == 0 || this.__AccessMode == 2)) ||
  40. (side == 1 && (this.__AccessMode == 1 || this.__AccessMode == 2))){
  41. this.emit("data_changed");
  42. }
  43. }).bind(this), 250);
  44. for (var i=0; i < 256; i++){
  45. this.__LP.push(new NESTile());
  46. this.__LP[i].listen("data_changed", handle_datachanged.bind(this, 0));
  47. this.__RP.push(new NESTile());
  48. this.__RP[i].listen("data_changed", handle_datachanged.bind(this, 1));
  49. }
  50. this.__palette = null;
  51. }
  52. get access_mode(){return this.__AccessMode;}
  53. set access_mode(m){
  54. if (!Utils.isInt(m))
  55. throw new TypeError("Access mode expected to be integer.");
  56. switch(m){
  57. case NESBank.ACCESSMODE_SPRITE:
  58. this.__AccessMode = NESBank.ACCESSMODE_SPRITE;
  59. this.emit("data_changed");
  60. break;
  61. case NESBank.ACCESSMODE_BACKGROUND:
  62. this.__AccessMode = NESBank.ACCESSMODE_BACKGROUND;
  63. this.emit("data_changed");
  64. break;
  65. case NESBank.ACCESSMODE_FULL:
  66. this.__AccessMode = NESBank.ACCESSMODE_FULL;
  67. this.emit("data_changed");
  68. break;
  69. }
  70. }
  71. get json(){
  72. JSON.stringify({
  73. LP: this.__LP.map(x=>x.base64),
  74. RP: this.__RP.map(x=>x.base64)
  75. });
  76. }
  77. get chr(){
  78. var buff = new Uint8Array(8192);
  79. var offset = 0;
  80. this.__LP.forEach(function(i){
  81. buff.set(i.chr, offset);
  82. offset += 16;
  83. });
  84. this.__RP.forEach(function(i){
  85. buff.set(i.chr, offset);
  86. offset += 16;
  87. });
  88. return buff;
  89. }
  90. set chr(buff){
  91. if (!(buff instanceof Uint8Array))
  92. throw new TypeError("Expected Uint8Array buffer.");
  93. if (buff.length !== 8192)
  94. throw new RangeError("Data buffer has invalid byte length.");
  95. var offset = 0;
  96. this.__LP.forEach((i) => {
  97. i.chr = buff.slice(offset, offset+15);
  98. offset += 16;
  99. });
  100. this.__RP.forEach((i) => {
  101. i.chr = buff.slice(offset, offset+15);
  102. offset += 16;
  103. });
  104. }
  105. get base64(){
  106. var b = "";
  107. var data = this.chr;
  108. for (var i = 0; i < data.length; i++) {
  109. b += String.fromCharCode(data[i]);
  110. }
  111. return window.btoa(b);
  112. }
  113. set base64(s){
  114. var b = window.atob(s);
  115. var len = b.length;
  116. if (b.length !== 8192){
  117. throw new Error("Base64 string contains invalid byte count.");
  118. }
  119. this.chr = b;
  120. }
  121. get palette(){return this.__palette;}
  122. set palette(p){
  123. if (p !== null && !(p instanceof NESPalette))
  124. throw new TypeError("Expected null or NESPalette object.");
  125. if (p !== this.__palette){
  126. this.__palette = p;
  127. }
  128. }
  129. get width(){return (this.__AccessMode == NESBank.ACCESSMODE_FULL) ? 256 : 128;}
  130. get height(){return 128;}
  131. get length(){return this.width * this.height;}
  132. get coloridx(){
  133. return new Proxy(this, {
  134. get:function(obj, prop){
  135. var len = obj.length * 8;
  136. if (prop === "length")
  137. return len;
  138. if (!Utils.isInt(prop))
  139. throw new TypeError("Expected integer index.");
  140. prop = parseInt(prop);
  141. if (prop < 0 || prop >= len)
  142. return NESPalette.Default[4];
  143. var res = LRIdx2TileIdxCo(prop, this.__AccessMode);
  144. var list = (res.lid === 0) ? obj.__LP : obj.__RP;
  145. return list[res.index].getPixelIndex(res.x, res.y);
  146. },
  147. set:function(obj, prop, value){
  148. if (!Utils.isInt(prop))
  149. throw new TypeError("Expected integer index.");
  150. if (!Utils.isInt(value))
  151. throw new TypeError("Color expected to be integer.");
  152. prop = parseInt(prop);
  153. value = parseInt(value);
  154. if (prop < 0 || prop >= len)
  155. throw new RangeError("Index out of bounds.");
  156. if (value < 0 || value >= 4)
  157. throw new RangeError("Color index out of bounds.");
  158. var res = LRIdx2TileIdxCo(prop, this.__AccessMode);
  159. var list = (res.lid === 0) ? obj.__LP : obj.__RP;
  160. list[res.index].setPixelIndex(res.x, res.y, value);
  161. return true;
  162. }
  163. });
  164. }
  165. get lp(){
  166. return new Proxy(this, {
  167. get: function(obj, prop){
  168. if (prop === "length")
  169. return obj.__LP.length;
  170. if (!Utils.isInt(prop))
  171. throw new TypeError("Expected integer index.");
  172. prop = parseInt(prop);
  173. if (prop < 0 || prop >= 256)
  174. throw new RangeError("Index out of bounds.");
  175. return obj.__LP[prop];
  176. },
  177. set: function(obj, prop, value){
  178. if (!Utils.isInt(prop))
  179. throw new TypeError("Expected integer index.");
  180. if (!(value instanceof NESTile))
  181. throw new TypeError("Can only assign NESTile objects.");
  182. prop = parseInt(prop);
  183. if (prop < 0 || prop >= 256)
  184. throw new RangeError("Index out of bounds.");
  185. obj.__LP[prop].copy(value);
  186. return true;
  187. }
  188. });
  189. }
  190. get rp(){
  191. return new Proxy(this, {
  192. get: function(obj, prop){
  193. if (prop === "length")
  194. return obj.__RP.length;
  195. if (!Utils.isInt(prop))
  196. throw new TypeError("Expected integer index.");
  197. prop = parseInt(prop);
  198. if (prop < 0 || prop >= 256)
  199. throw new RangeError("Index out of bounds.");
  200. return obj.__RP[prop];
  201. },
  202. set: function(obj, prop, value){
  203. if (!Utils.isInt(prop))
  204. throw new TypeError("Expected integer index.");
  205. if (!(value instanceof NESTile))
  206. throw new TypeError("Can only assign NESTile objects.");
  207. prop = parseInt(prop);
  208. if (prop < 0 || prop >= 256)
  209. throw new RangeError("Index out of bounds.");
  210. obj.__RP[prop].copy(value);
  211. return true;
  212. }
  213. });
  214. }
  215. copy(b){
  216. if (!(b instanceof NESBank))
  217. throw new TypeError("Expected NESBank object.");
  218. for (var i=0; i < 256; i++){
  219. this.lp[i] = b.lp[i];
  220. this.rp[i] = b.rp[i];
  221. }
  222. return this;
  223. }
  224. clone(){
  225. return (new NESBank()).copy(this);
  226. }
  227. getColor(x,y){
  228. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  229. return this.__default_pi[4];
  230. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  231. var list = (res.lid === 0) ? this.__LP : this.__RP;
  232. var pi = list[res.index].paletteIndex + ((res.lid === 0) ? 4 : 0);
  233. var ci = list[res.index].getPixelIndex(res.x, res.y);
  234. if (this.__palette !== null){
  235. return this.__palette.get_palette_color(pi, ci);
  236. }
  237. return NESPalette.Default[ci];
  238. }
  239. getColorIndex(x, y){
  240. if (x < 0 || x >= this.width || y < 0 || y >= this.height)
  241. return {pi: -1, ci:-1};
  242. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  243. var list = (res.lid === 0) ? this.__LP : this.__RP;
  244. return {
  245. pi: list[res.index].paletteIndex,
  246. ci: list[res.index].getPixelIndex(res.x, res.y)
  247. };
  248. }
  249. setColorIndex(x, y, ci, pi){
  250. if (x < 0 || x >= this.width || y < 0 || y > this.height)
  251. throw new RangeError("Coordinates out of bounds.");
  252. if (!Utils.isInt(pi))
  253. pi = -1;
  254. if (!Utils.isInt(ci))
  255. ci = 0;
  256. if (pi < 0){
  257. this.coloridx[(y*this.width)+x] = ci;
  258. } else {
  259. var res = LRIdx2TileIdxCo((y*this.width)+x, this.__AccessMode);
  260. var list = (res.lid === 0) ? this.__LP : this.__RP;
  261. list[res.index].paletteIndex = pi;
  262. list[res.index].setPixelIndex(res.x, res.y, ci);
  263. }
  264. return this;
  265. }
  266. }
  267. NESBank.ACCESSMODE_8K = 0;
  268. NESBank.ACCESSMODE_1K = 1;
  269. NESBank.ACCESSMODE_2K = 2;
  270. NESBank.ACCESSMODE_4K = 3;