"use strict"; import { hex, rgb2bgr, rle_unpack } from "../util"; import { ProjectWindows } from "../windows"; declare var Mousetrap; export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i:number]:number}; // TODO: separate view/controller export interface EditorContext { setCurrentEditor(div:JQuery, editing:JQuery, node:PixNode) : void; getPalettes(matchlen : number) : SelectablePalette[]; getTilemaps(matchlen : number) : SelectableTilemap[]; } export type SelectablePalette = { node:PixNode name:string palette:Uint32Array } export type SelectableTilemap = { node:PixNode name:string images:Uint8Array[] rgbimgs:Uint32Array[] // TODO: different palettes? } export type PixelEditorImageFormat = { w:number h:number count?:number bpp?:number np?:number bpw?:number sl?:number pofs?:number remap?:number[] brev?:boolean flip?:boolean destfmt?:PixelEditorImageFormat xform?:string skip?:number }; export type PixelEditorPaletteFormat = { pal?:number|string n?:number layout?:string }; export type PixelEditorPaletteLayout = [string, number, number][]; type PixelEditorMessage = { fmt : PixelEditorImageFormat palfmt : PixelEditorPaletteFormat bytestr : string palstr : string }; ///////////////// var pixel_re = /([0#]?)([x$%]|\d'[bh])([0-9a-f]+)(?:;.*$)?/gim; function convertToHexStatements(s:string) : string { // convert 'hex ....' asm format return s.replace(/(\shex\s+)([0-9a-f]+)/ig, function(m,hexprefix,hexstr) { var rtn = hexprefix; for (var i=0; i { var rtn = hexprefix + hexstr; rtn = rtn.replace(/0x/ig,'').replace(/,/ig,'') return rtn; }); return result; } function remapBits(x:number, arr:number[]) : number { if (!arr) return x; var y = 0; for (var i=0; i>(bitsperword-shift-bpp) : byte>>shift) & mask) << (p*bpp); } imgdata.push(color); shift += bpp; if (shift >= bitsperword) { ofs0 += 1; shift = 0; } } } images.push(new Uint8Array(imgdata)); } return images; } function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : number[] { if (fmt.destfmt) fmt = fmt.destfmt; var width = fmt.w; var height = fmt.h; var count = fmt.count || 1; var bpp = fmt.bpp || 1; var nplanes = fmt.np || 1; var bitsperword = fmt.bpw || 8; var wordsperline = fmt.sl || Math.ceil(fmt.w * bpp / bitsperword); var mask = (1 << bpp)-1; var pofs = fmt.pofs || wordsperline*height*count; var skip = fmt.skip || 0; var words; if (bitsperword <= 8) words = new Uint8Array(wordsperline*height*count*nplanes); else words = new Uint32Array(wordsperline*height*count*nplanes); for (var n=0; n> (p*bpp)) & mask; words[ofs + p*pofs + skip] |= (fmt.brev ? (c << (bitsperword-shift-bpp)) : (c << shift)); } shift += bpp; if (shift >= bitsperword) { ofs0 += 1; shift = 0; } } } } return words; } // TODO function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] { var result = []; for (var i=0; i> r0) & ((1<> g0) & ((1<> b0) & ((1<= 0) newpalette = convertPaletteBytes(palbytes, 0, rr, rr, gg, rr+gg, bb); else newpalette = convertPaletteBytes(palbytes, rr+gg, bb, rr, gg, 0, rr); } else { var paltable = PREDEF_PALETTES[pal]; if (paltable) { newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length-1)] | 0xff000000; }); } else { throw new Error("No palette named " + pal); } } return newpalette; } // TODO: illegal colors? var PREDEF_PALETTES = { 'nes':[ 0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000 ], 'ap2lores':[ (0x000000), (0xff00ff), (0x00007f), (0x7f007f), (0x007f00), (0x7f7f7f), (0x0000bf), (0x0000ff), (0xbf7f00), (0xffbf00), (0xbfbfbf), (0xff7f7f), (0x00ff00), (0xffff00), (0x00bf7f), (0xffffff), ], 'vcs':[ 0x000000,0x000000, 0x404040,0x404040, 0x6c6c6c,0x6c6c6c, 0x909090,0x909090, 0xb0b0b0,0xb0b0b0, 0xc8c8c8,0xc8c8c8, 0xdcdcdc,0xdcdcdc, 0xf4f4f4,0xf4f4f4, 0x004444,0x004444, 0x106464,0x106464, 0x248484,0x248484, 0x34a0a0,0x34a0a0, 0x40b8b8,0x40b8b8, 0x50d0d0,0x50d0d0, 0x5ce8e8,0x5ce8e8, 0x68fcfc,0x68fcfc, 0x002870,0x002870, 0x144484,0x144484, 0x285c98,0x285c98, 0x3c78ac,0x3c78ac, 0x4c8cbc,0x4c8cbc, 0x5ca0cc,0x5ca0cc, 0x68b4dc,0x68b4dc, 0x78c8ec,0x78c8ec, 0x001884,0x001884, 0x183498,0x183498, 0x3050ac,0x3050ac, 0x4868c0,0x4868c0, 0x5c80d0,0x5c80d0, 0x7094e0,0x7094e0, 0x80a8ec,0x80a8ec, 0x94bcfc,0x94bcfc, 0x000088,0x000088, 0x20209c,0x20209c, 0x3c3cb0,0x3c3cb0, 0x5858c0,0x5858c0, 0x7070d0,0x7070d0, 0x8888e0,0x8888e0, 0xa0a0ec,0xa0a0ec, 0xb4b4fc,0xb4b4fc, 0x5c0078,0x5c0078, 0x74208c,0x74208c, 0x883ca0,0x883ca0, 0x9c58b0,0x9c58b0, 0xb070c0,0xb070c0, 0xc084d0,0xc084d0, 0xd09cdc,0xd09cdc, 0xe0b0ec,0xe0b0ec, 0x780048,0x780048, 0x902060,0x902060, 0xa43c78,0xa43c78, 0xb8588c,0xb8588c, 0xcc70a0,0xcc70a0, 0xdc84b4,0xdc84b4, 0xec9cc4,0xec9cc4, 0xfcb0d4,0xfcb0d4, 0x840014,0x840014, 0x982030,0x982030, 0xac3c4c,0xac3c4c, 0xc05868,0xc05868, 0xd0707c,0xd0707c, 0xe08894,0xe08894, 0xeca0a8,0xeca0a8, 0xfcb4bc,0xfcb4bc, 0x880000,0x880000, 0x9c201c,0x9c201c, 0xb04038,0xb04038, 0xc05c50,0xc05c50, 0xd07468,0xd07468, 0xe08c7c,0xe08c7c, 0xeca490,0xeca490, 0xfcb8a4,0xfcb8a4, 0x7c1800,0x7c1800, 0x90381c,0x90381c, 0xa85438,0xa85438, 0xbc7050,0xbc7050, 0xcc8868,0xcc8868, 0xdc9c7c,0xdc9c7c, 0xecb490,0xecb490, 0xfcc8a4,0xfcc8a4, 0x5c2c00,0x5c2c00, 0x784c1c,0x784c1c, 0x906838,0x906838, 0xac8450,0xac8450, 0xc09c68,0xc09c68, 0xd4b47c,0xd4b47c, 0xe8cc90,0xe8cc90, 0xfce0a4,0xfce0a4, 0x2c3c00,0x2c3c00, 0x485c1c,0x485c1c, 0x647c38,0x647c38, 0x809c50,0x809c50, 0x94b468,0x94b468, 0xacd07c,0xacd07c, 0xc0e490,0xc0e490, 0xd4fca4,0xd4fca4, 0x003c00,0x003c00, 0x205c20,0x205c20, 0x407c40,0x407c40, 0x5c9c5c,0x5c9c5c, 0x74b474,0x74b474, 0x8cd08c,0x8cd08c, 0xa4e4a4,0xa4e4a4, 0xb8fcb8,0xb8fcb8, 0x003814,0x003814, 0x1c5c34,0x1c5c34, 0x387c50,0x387c50, 0x50986c,0x50986c, 0x68b484,0x68b484, 0x7ccc9c,0x7ccc9c, 0x90e4b4,0x90e4b4, 0xa4fcc8,0xa4fcc8, 0x00302c,0x00302c, 0x1c504c,0x1c504c, 0x347068,0x347068, 0x4c8c84,0x4c8c84, 0x64a89c,0x64a89c, 0x78c0b4,0x78c0b4, 0x88d4cc,0x88d4cc, 0x9cece0,0x9cece0, 0x002844,0x002844, 0x184864,0x184864, 0x306884,0x306884, 0x4484a0,0x4484a0, 0x589cb8,0x589cb8, 0x6cb4d0,0x6cb4d0, 0x7ccce8,0x7ccce8, 0x8ce0fc,0x8ce0fc ] }; var PREDEF_LAYOUTS : {[id:string]:PixelEditorPaletteLayout} = { 'nes':[ ['Screen Color', 0x00, 1], ['Background 0', 0x01, 3], ['Background 1', 0x05, 3], ['Background 2', 0x09, 3], ['Background 3', 0x0d, 3], ['Sprite 0', 0x11, 3], ['Sprite 1', 0x15, 3], ['Sprite 2', 0x19, 3], ['Sprite 3', 0x1d, 3] ], }; ///// function equalArrays(a:UintArray, b:UintArray) : boolean { if (a == null || b == null) return false; if (a.length !== b.length) return false; if (a === b) return true; for (var i=0; i { var out = new Uint8Array(im.length); for (var i=0; i { var out = new Uint32Array(im.length); for (var i=0; i 0) { newpalette = this.paloptions[this.palindex].palette; } } if (newpalette == null) { if (this.ncolors <= 2) newpalette = new Uint32Array([0xff000000, 0xffffffff]); else newpalette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO: more palettes } if (equalArrays(this.palette, newpalette)) return false; this.palette = newpalette; return true; } } function dedupPalette(cols : UintArray) : Uint32Array { var dup = new Map(); var res = new Uint32Array(cols.length); var ndups = 0; for (var i=0; i { this.rgbimgs.push(new Uint32Array([rgba])); }); return true; } getAllColors() { var arr = []; for (var i=0; i 0) { this.tilemap = this.tileoptions[this.tileindex].images; } } return !equalNestedArrays(oldtilemap, this.tilemap); } } export type MetaspriteEntry = { x:number, y:number, tile:number, attr:number }; export class MetaspriteCompositor extends Compositor { metadefs : MetaspriteEntry[]; constructor(context:EditorContext, metadefs) { super(context); this.metadefs = metadefs; } updateLeft() { // TODO return false; } updateRight() { this.updateRefs(); this.width = 16; // TODO this.height = 16; // TODO var idata = new Uint8Array(this.width * this.height); this.images = [idata]; this.metadefs.forEach((meta) => { // TODO }); return true; } } export class NESNametableConverter extends Compositor { cols : number; rows : number; baseofs : number; constructor(context:EditorContext) { super(context); } updateLeft() { // TODO return false; } updateRight() { if (!this.updateRefs() && equalArrays(this.words, this.left.words)) return false; this.words = this.left.words; this.cols = 32; this.rows = 30; this.width = this.cols * 8; this.height = this.rows * 8; this.baseofs = 0; var idata = new Uint8Array(this.width * this.height); this.images = [idata]; var a = 0; var attraddr; for (var row=0; row> 4) & 0x38) | ((a >> 2) & 0x07); var attr = this.words[attraddr]; var tag = name ^ (attr<<9) ^ 0x80000000; var i = row*this.cols*8*8 + col*8; var j = 0; var attrshift = (col&2) + ((a&0x40)>>4); var coloradd = ((attr >> attrshift) & 3) << 2; for (var y=0; y<8; y++) { for (var x=0; x<8; x++) { var color = t[j++]; if (color) color += coloradd; idata[i++] = color; } i += this.cols*8-8; } a++; } } // TODO return true; } } ///// UI CONTROLS export class ImageChooser { rgbimgs : Uint32Array[]; width : number; height : number; recreate(parentdiv:JQuery, onclick) { var agrid = $('
'); // grid (or 1) of preview images parentdiv.empty().append(agrid); var cscale = Math.max(2, Math.ceil(16/this.width)); // TODO var imgsperline = this.width <= 8 ? 16 : 8; // TODO var span = null; this.rgbimgs.forEach((imdata, i) => { var viewer = new Viewer(); viewer.width = this.width; viewer.height = this.height; viewer.recreate(); viewer.canvas.style.width = (viewer.width*cscale)+'px'; // TODO viewer.canvas.title = '$'+hex(i); viewer.updateImage(imdata); $(viewer.canvas).addClass('asset_cell'); $(viewer.canvas).click((e) => { onclick(i, viewer); }); if (!span) { span = $(''); agrid.append(span); } span.append(viewer.canvas); var brk = (i % imgsperline) == imgsperline-1; if (brk) { agrid.append($("
")); span = null; } }); } } function newDiv(parent?, cls? : string) { var div = $(document.createElement("div")); if (parent) div.appendTo(parent) if (cls) div.addClass(cls); return div; } export class CharmapEditor extends PixNode { context; parentdiv; fmt; chooser; constructor(context:EditorContext, parentdiv:JQuery, fmt:PixelEditorImageFormat) { super(); this.context = context; this.parentdiv = parentdiv; this.fmt = fmt; } updateLeft() { return true; } updateRight() { if (equalNestedArrays(this.rgbimgs, this.left.rgbimgs)) return false; this.rgbimgs = this.left.rgbimgs; var adual = newDiv(this.parentdiv.empty(), "asset_dual"); // contains grid and editor var agrid = newDiv(adual); var aeditor = newDiv(adual, "asset_editor").hide(); // contains editor, when selected // add image chooser grid var chooser = this.chooser = new ImageChooser(); chooser.rgbimgs = this.rgbimgs; chooser.width = this.fmt.w || 1; chooser.height = this.fmt.h || 1; chooser.recreate(agrid, (index, viewer) => { var escale = Math.ceil(192 / this.fmt.w); var editview = this.createEditor(aeditor, viewer, escale); this.context.setCurrentEditor(aeditor, $(viewer.canvas), this); this.rgbimgs[index] = viewer.rgbdata; }); // add palette selector // TODO: only view when editing? var palizer = this.left; if (palizer instanceof Palettizer && palizer.paloptions.length > 1) { var palselect = $(document.createElement('select')); palizer.paloptions.forEach((palopt, i) => { // TODO: full identifier var sel = $(document.createElement('option')).text(palopt.name).val(i).appendTo(palselect); if (i == (palizer as Palettizer).palindex) sel.attr('selected','selected'); }); palselect.appendTo(agrid).change((e) => { var index = $(e.target).val() as number; (palizer as Palettizer).palindex = index; palizer.refreshRight(); }); } return true; } createEditor(aeditor : JQuery, viewer : Viewer, escale : number) : PixEditor { var im = new PixEditor(); im.createWith(viewer); im.updateImage(); im.canvas.style.width = (viewer.width*escale)+'px'; // TODO im.makeEditable(this, aeditor, this.left.palette); return im; } } export class Viewer { width : number; height : number; canvas : HTMLCanvasElement; ctx : CanvasRenderingContext2D; imagedata : ImageData; rgbdata : Uint32Array; peerviewers : Viewer[]; recreate() { this.canvas = this.newCanvas(); this.imagedata = this.ctx.createImageData(this.width, this.height); this.rgbdata = new Uint32Array(this.imagedata.data.buffer); this.peerviewers = [this]; } createWith(pv : Viewer) { this.width = pv.width; this.height = pv.height; this.imagedata = pv.imagedata; this.rgbdata = pv.rgbdata; this.canvas = this.newCanvas(); this.peerviewers = [this, pv]; } newCanvas() : HTMLCanvasElement { var c = document.createElement('canvas'); c.width = this.width; c.height = this.height; //if (fmt.xform) c.style.transform = fmt.xform; c.classList.add("pixels"); c.classList.add("pixelated"); this.ctx = c.getContext('2d'); return c; } updateImage(imdata? : Uint32Array) { if (imdata) { this.rgbdata.set(imdata); } for (let v of this.peerviewers) { v.ctx.putImageData(this.imagedata, 0, 0); } } } class PixEditor extends Viewer { left : PixNode; palette : Uint32Array; curpalcol : number = -1; currgba : number; palbtns : JQuery[]; offscreen : Map = new Map(); getPositionFromEvent(e) { var x = Math.floor(e.offsetX * this.width / $(this.canvas).width()); var y = Math.floor(e.offsetY * this.height / $(this.canvas).height()); return {x:x, y:y}; } setPaletteColor(col: number) { col &= this.palette.length-1; if (this.curpalcol != col) { if (this.curpalcol >= 0) this.palbtns[this.curpalcol].removeClass('selected'); this.curpalcol = col; this.currgba = this.palette[col & this.palette.length-1]; this.palbtns[col].addClass('selected'); } } makeEditable(leftnode:PixNode, aeditor:JQuery, palette:Uint32Array) { this.left = leftnode; this.palette = palette; var dragcol; var dragging = false; var pxls = $(this.canvas); pxls.mousedown( (e) => { var pos = this.getPositionFromEvent(e); dragcol = this.getPixel(pos.x, pos.y) == this.currgba ? this.palette[0] : this.currgba; this.setPixel(pos.x, pos.y, this.currgba); dragging = true; $(document).mouseup( (e) => { var pos = this.getPositionFromEvent(e); this.setPixel(pos.x, pos.y, dragcol); dragging = false; this.commit(); $(document).off('mouseup'); }); }) .mousemove( (e) => { var pos = this.getPositionFromEvent(e); if (dragging) { this.setPixel(pos.x, pos.y, dragcol); } }); /* Mousetrap.bind('ctrl+shift+h', this.flipX.bind(this)); Mousetrap.bind('ctrl+shift+v', this.flipY.bind(this)); Mousetrap.bind('ctrl+shift+9', this.rotate90.bind(this)); Mousetrap.bind('ctrl+shift+left', this.translate.bind(this, -1, 0)); Mousetrap.bind('ctrl+shift+right', this.translate.bind(this, 1, 0)); Mousetrap.bind('ctrl+shift+up', this.translate.bind(this, 0, -1)); Mousetrap.bind('ctrl+shift+down', this.translate.bind(this, 0, 1)); */ // TODO: remove when unbound aeditor.empty(); aeditor.append(this.canvas); aeditor.append(this.createPaletteButtons()); this.setPaletteColor(1); } getPixel(x:number, y:number) : number { x = Math.round(x); y = Math.round(y); if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return this.offscreen[x+','+y] | this.palette[0]; } else { var ofs = x+y*this.width; return this.rgbdata[ofs]; } } setPixel(x:number, y:number, rgba:number) : void { x = Math.round(x); y = Math.round(y); if (x < 0 || x >= this.width || y < 0 || y >= this.height) { this.offscreen[x+','+y] = rgba; } else { var ofs = x+y*this.width; var oldrgba = this.rgbdata[ofs]; if (oldrgba != rgba) { this.rgbdata[ofs] = rgba; this.updateImage(); } } } createPaletteButtons() { this.palbtns = []; var span = newDiv(null, "asset_toolbar"); for (var i=0; i number) { var i = 0; var pixels = new Uint32Array(this.rgbdata.length); for (var y=0; y { var xx = x + 0.5 - this.width/2.0; var yy = y + 0.5 - this.height/2.0; var xx2 = xx*c1 - yy*s1 + this.width/2.0 - 0.5; var yy2 = yy*c1 + xx*s1 + this.height/2.0 - 0.5; return this.getPixel(xx, yy); }); } rotate90() { this.rotate(90); } flipX() { this.remapPixels((x,y) => { return this.getPixel(this.width-1-x, y); }); } flipY() { this.remapPixels((x,y) => { return this.getPixel(x, this.height-1-y); }); } translate(dx:number, dy:number) { this.remapPixels((x,y) => { return this.getPixel(x+dx, y+dy); }); } }