pixel editor lazy updates

This commit is contained in:
Steven Hugg 2019-03-26 14:29:47 -04:00
parent 2889ef33bd
commit 6cea0772bf
5 changed files with 121 additions and 37 deletions

View File

@ -502,4 +502,11 @@ div.asset_editor {
padding:16px;
margin:16px;
background-color:#999;
display:flex;
flex-direction:column;
align-items:center;
}
div.asset_toolbar {
padding:8px;
margin:8px;
}

View File

@ -108,6 +108,7 @@ TODO:
- metasprites
- update nested data, palette/tile refs properly
- throw errors when bad/no refs
- careful with mouse capture out of frame
WEB WORKER FORMAT

View File

@ -317,6 +317,33 @@ var PREDEF_LAYOUTS : {[id:string]:PixelEditorPaletteLayout} = {
/////
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<a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
}
function equalNestedArrays(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<a.length; i++) {
if (!equalArrays(a[i], b[i]))
return false;
}
return true;
}
export abstract class PixNode {
left : PixNode; // toward text editor
right : PixNode; // toward pixel editor
@ -326,8 +353,8 @@ export abstract class PixNode {
rgbimgs? : Uint32Array[]; // array of rgba imgages
palette? : Uint32Array; // array of rgba
abstract updateLeft(); // update coming from right
abstract updateRight(); // update coming from left
abstract updateLeft() : boolean; // update coming from right
abstract updateRight() : boolean; // update coming from left
refreshLeft() {
var p : PixNode = this;
@ -364,20 +391,25 @@ abstract class CodeProjectDataNode extends PixNode {
export class FileDataNode extends CodeProjectDataNode {
constructor(project:ProjectWindows, fileid:string, data:Uint8Array) {
constructor(project:ProjectWindows, fileid:string) {
super();
this.project = project;
this.fileid = fileid;
this.label = fileid;
this.words = data;
}
updateLeft() {
//if (equalArrays(this.words, this.right.words)) return false;
this.words = this.right.words;
if (this.project) {
this.project.updateFile(this.fileid, this.words as Uint8Array);
}
return true;
}
updateRight() {
if (this.project) {
this.words = this.project.project.getFile(this.fileid) as Uint8Array;
}
return true;
}
}
@ -386,12 +418,12 @@ export class TextDataNode extends CodeProjectDataNode {
start : number;
end : number;
constructor(project:ProjectWindows, fileid:string, label:string, text:string, start:number, end:number) {
// TODO: what if file size/layout changes?
constructor(project:ProjectWindows, fileid:string, label:string, start:number, end:number) {
super();
this.project = project;
this.fileid = fileid;
this.label = label;
this.text = text;
this.start = start;
this.end = end;
}
@ -407,12 +439,17 @@ export class TextDataNode extends CodeProjectDataNode {
this.project.updateFile(this.fileid, this.text);
//this.project.replaceTextRange(this.fileid, this.start, this.end, datastr);
}
return true;
}
updateRight() {
if (this.project) {
this.text = this.project.project.getFile(this.fileid) as string;
}
var datastr = this.text.substring(this.start, this.end);
datastr = convertToHexStatements(datastr); // TODO?
var words = parseHexWords(datastr);
this.words = words; //new Uint8Array(words); // TODO: 16/32?
return true;
}
}
@ -422,9 +459,11 @@ export class Compressor extends PixNode {
updateLeft() {
// TODO: can't modify length of rle bytes
return false;
}
updateRight() {
this.words = rle_unpack(new Uint8Array(this.left.words));
return true;
}
}
@ -440,13 +479,17 @@ export class Mapper extends PixNode {
this.fmt = fmt;
}
updateLeft() {
//if (equalNestedArrays(this.images, this.right.images)) return false;
this.images = this.right.images;
this.words = convertImagesToWords(this.images, this.fmt);
return true;
}
updateRight() {
if (equalArrays(this.words, this.left.words)) return false;
// convert each word array to images
this.words = this.left.words;
this.images = convertWordsToImages(this.words, this.fmt);
return true;
}
}
@ -481,18 +524,21 @@ export class Palettizer extends PixNode {
updateLeft() {
this.rgbimgs = this.right.rgbimgs;
var pal = new RGBAPalette(this.palette);
this.images = this.rgbimgs.map( (im:Uint32Array) => {
var newimages = this.rgbimgs.map( (im:Uint32Array) => {
var out = new Uint8Array(im.length);
for (var i=0; i<im.length; i++) {
out[i] = pal.indexOf(im[i]);
}
return out;
});
// have to do it this way b/c pixel editor modifies arrays
//if (equalNestedArrays(newimages, this.images)) return false;
this.images = newimages;
return true;
}
updateRight() {
this.updateRefs();
if (!this.updateRefs() && equalNestedArrays(this.images, this.left.images)) return false;
this.images = this.left.images;
if (!this.palette || !this.images) return;
var mask = this.palette.length - 1; // must be power of 2
// for each image, map bytes to RGB colors
this.rgbimgs = this.images.map( (im:Uint8Array) => {
@ -502,20 +548,25 @@ export class Palettizer extends PixNode {
}
return out;
});
return true;
}
updateRefs() {
var newpalette;
if (this.context != null) {
this.paloptions = this.context.getPalettes(this.ncolors);
if (this.paloptions && this.paloptions.length > 0) {
this.palette = this.paloptions[this.palindex].palette;
newpalette = this.paloptions[this.palindex].palette;
}
}
if (this.palette == null) {
if (newpalette == null) {
if (this.ncolors <= 2)
this.palette = new Uint32Array([0xff000000, 0xffffffff]);
newpalette = new Uint32Array([0xff000000, 0xffffffff]);
else
this.palette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO: more palettes
newpalette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO: more palettes
}
if (equalArrays(this.palette, newpalette)) return false;
this.palette = newpalette;
return true;
}
}
@ -533,9 +584,9 @@ function dedupPalette(cols : UintArray) : Uint32Array {
}
return res;
}
export class PaletteFormatToRGB extends PixNode {
words : UintArray;
rgbimgs : Uint32Array[];
palette : Uint32Array;
@ -548,8 +599,10 @@ export class PaletteFormatToRGB extends PixNode {
}
updateLeft() {
//TODO
return true;
}
updateRight() {
if (equalArrays(this.words, this.left.words)) return false;
this.words = this.left.words;
this.palette = dedupPalette(convertPaletteFormat(this.words, this.palfmt));
this.layout = PREDEF_LAYOUTS[this.palfmt.layout];
@ -557,6 +610,7 @@ export class PaletteFormatToRGB extends PixNode {
this.palette.forEach( (rgba:number) => {
this.rgbimgs.push(new Uint32Array([rgba]));
});
return true;
}
getAllColors() {
var arr = [];
@ -581,13 +635,15 @@ export abstract class Compositor extends PixNode {
super();
this.context = context;
}
updateRefs() {
updateRefs() : boolean {
var oldtilemap = this.tilemap;
if (this.context != null) {
this.tileoptions = this.context.getTilemaps(256);
if (this.tileoptions && this.tileoptions.length > 0) {
this.tilemap = this.tileoptions[this.tileindex].images;
}
}
return !equalNestedArrays(oldtilemap, this.tilemap);
}
}
@ -605,6 +661,7 @@ export class MetaspriteCompositor extends Compositor {
}
updateLeft() {
// TODO
return false;
}
updateRight() {
this.updateRefs();
@ -615,6 +672,7 @@ export class MetaspriteCompositor extends Compositor {
this.metadefs.forEach((meta) => {
// TODO
});
return true;
}
}
@ -628,11 +686,11 @@ export class NESNametableConverter extends Compositor {
}
updateLeft() {
// TODO
return false;
}
updateRight() {
if (!this.updateRefs() && equalArrays(this.words, this.left.words)) return false;
this.words = this.left.words;
this.updateRefs();
if (!this.words || !this.tilemap) return;
this.cols = 32;
this.rows = 30;
this.width = this.cols * 8;
@ -665,6 +723,7 @@ export class NESNametableConverter extends Compositor {
}
}
// TODO
return true;
}
}
@ -682,7 +741,6 @@ export class ImageChooser {
var cscale = Math.max(2, Math.ceil(16/this.width)); // TODO
var imgsperline = this.width <= 8 ? 16 : 8; // TODO
var span = null;
if (!this.rgbimgs) return;
this.rgbimgs.forEach((imdata, i) => {
var viewer = new Viewer();
viewer.width = this.width;
@ -730,9 +788,11 @@ export class CharmapEditor extends PixNode {
}
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);
@ -765,6 +825,7 @@ export class CharmapEditor extends PixNode {
palizer.refreshRight();
});
}
return true;
}
createEditor(aeditor : JQuery, viewer : Viewer, escale : number) : PixEditor {
@ -901,7 +962,7 @@ class PixEditor extends Viewer {
createPaletteButtons() {
this.palbtns = [];
var span = $(document.createElement('div'));
var span = newDiv(null, "asset_toolbar");
for (var i=0; i<this.palette.length; i++) {
var btn = $(document.createElement('button')).addClass('palbtn');
var rgb = this.palette[i] & 0xffffff;

View File

@ -974,8 +974,9 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
}
if (div) {
this.cureditordiv = div;
this.cureditordiv.show(timeout);
this.cureditordiv.show();
this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"});
//setTimeout(() => { this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}) }, timeout);
}
}
if (this.cureditelem) {
@ -1039,37 +1040,42 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
return result;
}
addPaletteEditorViews(parentdiv:JQuery, words, palette, layout, allcolors, callback) {
addPaletteEditorViews(parentdiv:JQuery, pal2rgb:pixed.PaletteFormatToRGB, callback) {
var adual = $('<div class="asset_dual"/>').appendTo(parentdiv);
var aeditor = $('<div class="asset_editor"/>').hide(); // contains editor, when selected
// TODO: they need to update when refreshed from right
var allrgbimgs = [];
allcolors.forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker)
pal2rgb.getAllColors().forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker)
var atable = $('<table/>').appendTo(adual);
aeditor.appendTo(adual);
// make default layout if not exists
var layout = pal2rgb.layout;
if (!layout) {
var len = palette.length;
var len = pal2rgb.palette.length;
var imgsperline = len > 32 ? 8 : 4; // TODO: use 'n'?
layout = [];
for (var i=0; i<len; i+=imgsperline) {
layout.push(["", i, Math.min(len-i,imgsperline)]);
}
}
function updateCell(cell, j) {
var val = pal2rgb.words[j];
var rgb = pal2rgb.palette[j];
var hexcol = '#'+hex(rgb2bgr(rgb),6);
var textcol = (rgb & 0x008000) ? 'black' : 'white';
cell.text(hex(val,2)).css('background-color',hexcol).css('color',textcol);
}
// iterate over each row of the layout
layout.forEach( ([name, start, len]) => {
if (start < palette.length) { // skip row if out of range
if (start < pal2rgb.palette.length) { // skip row if out of range
var arow = $('<tr/>').appendTo(atable);
$('<td/>').text(name).appendTo(arow);
var inds = [];
for (var k=start; k<start+len; k++)
inds.push(k);
inds.forEach( (i) => {
var val = words[i];
var rgb = palette[i];
var hexcol = '#'+hex(rgb2bgr(rgb),6);
var textcol = (rgb & 0x008000) ? 'black' : 'white';
var cell = $('<td/>').addClass('asset_cell asset_editable').text(hex(val,2)).css('background-color',hexcol).css('color',textcol).appendTo(arow);
var cell = $('<td/>').addClass('asset_cell asset_editable').appendTo(arow);
updateCell(cell, i);
cell.click((e) => {
var chooser = new pixed.ImageChooser();
chooser.rgbimgs = allrgbimgs;
@ -1077,6 +1083,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
chooser.height = 1;
chooser.recreate(aeditor, (index, newvalue) => {
callback(i, index);
updateCell(cell, i);
});
this.setCurrentEditor(aeditor, cell);
});
@ -1106,13 +1113,14 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
firstnode.refreshRight();
// TODO: add view objects
// TODO: show which one is selected?
this.addPaletteEditorViews(parentdiv, firstnode.words,
pal2rgb.palette, pal2rgb.layout, pal2rgb.getAllColors(),
this.addPaletteEditorViews(parentdiv, pal2rgb,
(index, newvalue) => {
console.log('set entry', index, '=', newvalue);
// TODO: this forces update of palette rgb colors and file data
firstnode.words[index] = newvalue;
//firstnode.refreshRight();
firstnode.refreshLeft();
pal2rgb.words = null;
pal2rgb.updateRight();
pal2rgb.refreshLeft();
});
}
@ -1124,16 +1132,17 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
// TODO: only refresh when needed
if (fileid.endsWith('.chr') && data instanceof Uint8Array) {
// is this a NES CHR?
let node = new pixed.FileDataNode(projectWindows, fileid, data);
let node = new pixed.FileDataNode(projectWindows, fileid);
const neschrfmt = {w:8,h:8,bpp:1,count:(data.length>>4),brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO
this.addPixelEditor(filediv, node, neschrfmt);
this.registerAsset("charmap", node, true);
nassets++;
} else if (typeof data === 'string') {
let textfrags = this.scanFileTextForAssets(fileid, data);
for (let frag of textfrags) {
if (frag.fmt) {
let label = fileid; // TODO: label
let node : pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, data, frag.start, frag.end);
let node : pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end);
let first = node;
// rle-compressed?
if (frag.fmt.comp == 'rletag') {
@ -1194,6 +1203,10 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext {
console.log("Found " + this.rootnodes.length + " assets");
this.deferrednodes.forEach((node) => { node.refreshRight(); });
this.deferrednodes = [];
} else {
for (var node of this.rootnodes) {
node.refreshRight();
}
}
}

View File

@ -13,13 +13,15 @@ describe('Pixel editor', function() {
var palfmt = {pal:332,n:16};
var paldatastr = " 0x00, 0x03, 0x19, 0x50, 0x52, 0x07, 0x1f, 0x37, 0xe0, 0xa4, 0xfd, 0xff, 0x38, 0x70, 0x7f, 0xf8, ";
var node4 = new pixed.TextDataNode(null, null, null, paldatastr, 0, paldatastr.length);
var node4 = new pixed.TextDataNode(null, null, null, 0, paldatastr.length);
node4.text = paldatastr;
var node5 = new pixed.PaletteFormatToRGB(palfmt);
node4.addRight(node5);
node4.refreshRight();
var datastr = "1,2, 0x00,0x00,0xef,0xef,0xe0,0x00,0x00, 0x00,0xee,0xee,0xfe,0xee,0xe0,0x00, 0x0e,0xed,0xef,0xef,0xed,0xee,0x00, 0x0e,0xee,0xdd,0xdd,0xde,0xee,0x00, 0x0e,0xee,0xed,0xde,0xee,0xee,0x00, 0x00,0xee,0xee,0xde,0xee,0xe0,0x00, 0x00,0xee,0xee,0xde,0xee,0xe0,0x00, 0x00,0x00,0xed,0xdd,0xe0,0x00,0x0d, 0xdd,0xdd,0xee,0xee,0xed,0xdd,0xd0, 0x0d,0xee,0xee,0xee,0xee,0xee,0x00, 0x0e,0xe0,0xee,0xee,0xe0,0xee,0x00, 0x0e,0xe0,0xee,0xee,0xe0,0xee,0x00, 0x0e,0xe0,0xdd,0xdd,0xd0,0xde,0x00, 0x0d,0x00,0xee,0x0e,0xe0,0x0d,0x00, 0x00,0x00,0xed,0x0e,0xe0,0x00,0x00, 0x00,0x0d,0xdd,0x0d,0xdd,0x00,0x18,";
var node1 = new pixed.TextDataNode(null, null, null, datastr, 0, datastr.length);
var node1 = new pixed.TextDataNode(null, null, null, 0, datastr.length);
node1.text = datastr;
var node2 = new pixed.Mapper(fmt);
node1.addRight(node2);
var node3 = new pixed.Palettizer(null, fmt);