A pixel art painter geared specifically at NES pixel art. Includes export for .chr binary file as well as palette and namespace data.

252 lines
6.2KB

  1. import GlobalEvents from "/app/js/common/EventCaller.js";
  2. import Utils from "/app/js/common/Utils.js";
  3. import NESBank from "/app/js/models/NESBank.js";
  4. import NESPalette from "/app/js/models/NESPalette.js";
  5. const BLI_TEMPLATE = "bank-list-item-template";
  6. const BLI_CANVAS = "bank-img";
  7. const BLI_TITLE = "title";
  8. const BLI_SELECTED = "list-item-selected";
  9. var Banks = {};
  10. var CurrentBank = "";
  11. function HANDLE_BankClick(e){
  12. var name = this.getAttribute("bankname");
  13. if (name !== CurrentBank){
  14. if (CurrentBank !== "")
  15. Banks[CurrentBank].el.classList.remove(BLI_SELECTED);
  16. CurrentBank = name;
  17. Banks[CurrentBank].el.classList.add(BLI_SELECTED);
  18. GlobalEvents.emit("change_surface", Banks[CurrentBank].bank);
  19. }
  20. }
  21. function SetElBankName(el, name){
  22. el.setAttribute("bankname", name);
  23. var sel = el.querySelector("." + BLI_TITLE);
  24. if (sel){
  25. sel.innerHTML = name;
  26. }
  27. }
  28. var RenderBankToEl = Utils.throttle(function(el, bank){
  29. var cnv = el.querySelector("." + BLI_CANVAS);
  30. var ctx = cnv.getContext("2d");
  31. var cw = (cnv.clientWidth > 0) ? Math.floor(cnv.clientWidth) : Math.floor(cnv.width);
  32. var ch = (cnv.clientHeight > 0) ? Math.floor(cnv.clientHeight) : Math.floor(cnv.height);
  33. if (cw <= 0 || ch <= 0){return;}
  34. var ctximg = ctx.getImageData(0,0,cw,ch);
  35. var idat = ctximg.data;
  36. var PutPixel = (i,j,s,c) => {
  37. i = Math.round(i);
  38. j = Math.round(j);
  39. s = Math.ceil(s);
  40. var r = parseInt(c.substring(1, 3), 16);
  41. var g = parseInt(c.substring(3, 5), 16);
  42. var b = parseInt(c.substring(5, 7), 16);
  43. for (var y=j; y < j+s; y++){
  44. for (var x=i; x < i+s; x++){
  45. if (x >= 0 && x < cw && y >= 0 && y < ch){
  46. var index = (y*cw*4) + (x*4);
  47. idat[index] = r;
  48. idat[index+1] = g;
  49. idat[index+2] = b;
  50. idat[index+3] = 255;
  51. }
  52. }
  53. }
  54. };
  55. var pal = bank.palette;
  56. if (pal === null){return;}
  57. var scale = Math.min(
  58. cw/bank.width,
  59. ch/bank.height
  60. );
  61. var offX = Math.floor((cw - (bank.width*scale)) * 0.5);
  62. var offY = Math.floor((ch - (bank.height*scale)) * 0.5);
  63. ctx.save();
  64. ctx.fillStyle = NESPalette.Default[4];
  65. ctx.fillRect(0,0,cw,ch);
  66. for (let j=0; j < bank.height; j++){
  67. var y = (j*scale) + offY;
  68. for (let i=0; i < bank.width; i++){
  69. var x = (i*scale) + offX;
  70. if (x >= 0 && x < cw && y >= 0 && y < ch){
  71. var color = NESPalette.Default[4];
  72. var pinfo = bank.getColorIndex(i, j);
  73. var color = (pinfo.ci >= 0) ? NESPalette.Default[pinfo.ci] : NESPalette.Default[4];
  74. PutPixel(x,y,scale,color);
  75. }
  76. }
  77. }
  78. ctx.putImageData(ctximg, 0, 0);
  79. ctx.restore();
  80. }, 500); // Only update twice a second.
  81. function HANDLE_BankDataChange(bank, e){
  82. RenderBankToEl(this, bank);
  83. }
  84. function ConnectElementToBank(el, bank){
  85. bank.listen("data_changed", HANDLE_BankDataChange.bind(el, bank));
  86. }
  87. function CreateBankDOMEntry(name, bank){
  88. var baseel = document.querySelector("." + BLI_TEMPLATE);
  89. if (!baseel){
  90. console.log("WARNING: Failed to find bank list item template.");
  91. return null;
  92. }
  93. var el = baseel.cloneNode(true);
  94. el.classList.remove(BLI_TEMPLATE);
  95. el.classList.remove("hidden");
  96. el.setAttribute("bankname", name);
  97. ConnectElementToBank(el, bank);
  98. SetElBankName(el, name);
  99. el.addEventListener("click", HANDLE_BankClick);
  100. baseel.parentNode.appendChild(el);
  101. setTimeout(()=>{
  102. RenderBankToEl(el, bank);
  103. }, 500); // Make the render call in about a half second. Allow DOM time to catch up?
  104. return el;
  105. }
  106. class CTRLBanksStore{
  107. constructor(){
  108. var HANDLE_ChangeSurface = function(surf){
  109. if (!(surf instanceof NESBank)){
  110. // TODO: Unselect any current bank element.
  111. CurrentBankIndex = "";
  112. } else {
  113. if (Banks.length <= 0 || (CurrentBank !== "" && Banks[CurrentBank].bank !== surf)){
  114. console.log("WARNING: Bank object being set outside of Bank Store.");
  115. }
  116. }
  117. }
  118. GlobalEvents.listen("change_surface", HANDLE_ChangeSurface);
  119. }
  120. get length(){
  121. return Object.keys(Banks).length;
  122. }
  123. get json(){
  124. var data = [];
  125. Object.keys(Banks).forEach((key) => {
  126. if (Banks.hasOwnProperty(key)){
  127. data.push({name:key, data:Banks[key].bank.base64});
  128. }
  129. });
  130. return JSON.stringify(data);
  131. }
  132. initialize(){
  133. if (this.length <= 0){
  134. this.createBank("Bank");
  135. }
  136. return this;
  137. }
  138. createBank(name, bbase64){
  139. if (!(name in Banks)){
  140. var bank = new NESBank();
  141. if (typeof(bbase64) === "string"){
  142. try {
  143. bank.base64 = bbase64;
  144. } catch (e) {
  145. console.log("Failed to create Bank. " + e.toString());
  146. bank = null;
  147. }
  148. }
  149. if (bank !== null){
  150. var el = CreateBankDOMEntry(name, bank);
  151. if (el){
  152. Banks[name] = {bank:bank, el:el};
  153. //Banks.push([name, bank, el]);
  154. if (this.length <= 1){
  155. Banks[name].el.click();
  156. //GlobalEvents.emit("change_surface", bank);
  157. }
  158. }
  159. }
  160. }
  161. return this;
  162. }
  163. removeBank(name){
  164. if (name in Banks){
  165. if (name === CurrentBank){
  166. var keys = Object.keys(Banks);
  167. if (keys.length > 1){
  168. CurrentBank = (keys[0] !== name) ? keys[0] : keys[1];
  169. } else {
  170. CurrentBank = "";
  171. }
  172. }
  173. Banks[name].el.parentNode.removeChild(Banks[name].el);
  174. delete Banks[name];
  175. if (CurrentBank !== ""){
  176. // TODO: Activate new Bank.
  177. }
  178. }
  179. return this;
  180. }
  181. renameBank(name, newname){
  182. if ((name in Banks) && !(newname in Banks)){
  183. Banks[newname] = Banks[name];
  184. delete Banks[name];
  185. SetElBankName(Banks[newname].el, newname);
  186. }
  187. return this;
  188. }
  189. activateBank(name){
  190. if (CurrentBank !== name && (name in Banks)){
  191. Banks[name].el.click();
  192. //CurrentBank = name;
  193. //GlobalEvents.emit("change_surface", Banks[CurrentBank].bank);
  194. }
  195. return this;
  196. }
  197. clear(){
  198. Object.keys(Banks).forEach((item) => {
  199. item.el.parentNode.removeChild(item.el);
  200. });
  201. Banks = {};
  202. if (CurrentBank !== "")
  203. GlobalEvents.emit("change_surface", null);
  204. CurrentBank = "";
  205. }
  206. }
  207. const instance = new CTRLBanksStore();
  208. export default instance;