8bitworkshop/src/pixed/pixeleditor.ts

1051 lines
30 KiB
TypeScript

"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<hexstr.length; i+=2) {
rtn += '0x'+hexstr.substr(i,2)+',';
}
return rtn;
});
}
export function parseHexWords(s:string) : number[] {
var arr = [];
var m;
while (m = pixel_re.exec(s)) {
var n;
if (m[2].startsWith('%') || m[2].endsWith("b"))
n = parseInt(m[3],2);
else if (m[2].startsWith('x') || m[2].startsWith('$') || m[2].endsWith('h'))
n = parseInt(m[3],16);
else
n = parseInt(m[3]);
arr.push(n);
}
return arr;
}
export function replaceHexWords(s:string, words:UintArray) : string {
var result = "";
var m;
var li = 0;
var i = 0;
while (m = pixel_re.exec(s)) {
result += s.slice(li, pixel_re.lastIndex - m[0].length);
li = pixel_re.lastIndex;
if (m[2].startsWith('%'))
result += m[1] + "%" + words[i++].toString(2);
else if (m[2].endsWith('b'))
result += m[1] + m[2] + words[i++].toString(2); // TODO
else if (m[2].endsWith('h'))
result += m[1] + m[2] + words[i++].toString(16); // TODO
else if (m[2].startsWith('x'))
result += m[1] + "x" + hex(words[i++]);
else if (m[2].startsWith('$'))
result += m[1] + "$" + hex(words[i++]);
else
result += m[1] + words[i++].toString();
}
result += s.slice(li);
// convert 'hex ....' asm format
result = result.replace(/(\shex\s+)([,x0-9a-f]+)/ig, (m,hexprefix,hexstr) => {
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<arr.length; i++) {
var s = arr[i];
if (s < 0) {
s = -s-1;
y ^= 1 << s;
}
if (x & (1 << i)) {
y ^= 1 << s;
}
}
return y;
}
function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] {
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(width * bpp / bitsperword);
var mask = (1 << bpp)-1;
var pofs = fmt.pofs || wordsperline*height*count;
var skip = fmt.skip || 0;
var images = [];
for (var n=0; n<count; n++) {
var imgdata = [];
for (var y=0; y<height; y++) {
var yp = fmt.flip ? height-1-y : y;
var ofs0 = n*wordsperline*height + yp*wordsperline;
var shift = 0;
for (var x=0; x<width; x++) {
var color = 0;
var ofs = remapBits(ofs0, fmt.remap);
for (var p=0; p<nplanes; p++) {
var byte = words[ofs + p*pofs + skip];
color |= ((fmt.brev ? byte>>(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<count; n++) {
var imgdata = images[n];
var i = 0;
for (var y=0; y<height; y++) {
var yp = fmt.flip ? height-1-y : y;
var ofs0 = n*wordsperline*height + yp*wordsperline;
var shift = 0;
for (var x=0; x<width; x++) {
var color = imgdata[i++];
var ofs = remapBits(ofs0, fmt.remap);
for (var p=0; p<nplanes; p++) {
var c = (color >> (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<arr.length; i++) {
var d = arr[i];
var rgb = 0xff000000;
rgb |= ((d >> r0) & ((1<<r1)-1)) << (0+8-r1);
rgb |= ((d >> g0) & ((1<<g1)-1)) << (8+8-g1);
rgb |= ((d >> b0) & ((1<<b1)-1)) << (16+8-b1);
result.push(rgb);
}
return result;
}
export function getPaletteLength(palfmt: PixelEditorPaletteFormat) : number {
var pal = palfmt.pal;
if (typeof pal === 'number') {
var rr = Math.floor(Math.abs(pal/100) % 10);
var gg = Math.floor(Math.abs(pal/10) % 10);
var bb = Math.floor(Math.abs(pal) % 10);
return 1<<(rr+gg+bb);
} else {
var paltable = PREDEF_PALETTES[pal];
if (paltable) {
return paltable.length;
} else {
throw new Error("No palette named " + pal);
}
}
}
function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteFormat) : number[] {
var pal = palfmt.pal;
var newpalette;
if (typeof pal === 'number') {
var rr = Math.floor(Math.abs(pal/100) % 10);
var gg = Math.floor(Math.abs(pal/10) % 10);
var bb = Math.floor(Math.abs(pal) % 10);
// TODO: n
if (pal >= 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<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
words? : UintArray; // file data
images? : Uint8Array[]; // array of indexed image data
rgbimgs? : Uint32Array[]; // array of rgba imgages
palette? : Uint32Array; // array of rgba
abstract updateLeft() : boolean; // update coming from right
abstract updateRight() : boolean; // update coming from left
refreshLeft() {
var p : PixNode = this;
while (p) {
p.updateLeft();
p = p.left;
}
}
refreshRight() {
var p : PixNode = this;
while (p) {
p.updateRight();
p = p.right;
}
}
addRight(node : PixNode) {
this.right = node;
node.left = this;
return node;
}
addLeft(node : PixNode) {
this.left = node;
node.right = this;
return node;
}
}
abstract class CodeProjectDataNode extends PixNode {
project : ProjectWindows;
fileid : string;
label : string;
words : UintArray;
}
export class FileDataNode extends CodeProjectDataNode {
constructor(project:ProjectWindows, fileid:string) {
super();
this.project = project;
this.fileid = fileid;
this.label = fileid;
}
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;
}
}
export class TextDataNode extends CodeProjectDataNode {
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.start = start;
this.end = end;
}
updateLeft() {
if (this.right.words.length != this.words.length)
throw "Expected " + this.words.length + " bytes; image has " + this.right.words.length;
this.words = this.right.words;
// TODO: reload editors?
var datastr = this.text.substring(this.start, this.end);
datastr = replaceHexWords(datastr, this.words);
this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end);
if (this.project) {
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;
}
}
export class Compressor extends PixNode {
words : UintArray;
updateLeft() {
// TODO: can't modify length of rle bytes
return false;
}
updateRight() {
this.words = rle_unpack(new Uint8Array(this.left.words));
return true;
}
}
export class Mapper extends PixNode {
fmt : PixelEditorImageFormat;
words : UintArray;
images : Uint8Array[];
constructor(fmt) {
super();
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;
}
}
class RGBAPalette {
palcols;
constructor(palcols : Uint32Array) {
this.palcols = palcols;
}
indexOf(rgba : number) : number {
return this.palcols.indexOf(rgba);
}
}
export class Palettizer extends PixNode {
images : Uint8Array[];
rgbimgs : Uint32Array[];
palette : Uint32Array;
ncolors : number;
context : EditorContext;
paloptions : SelectablePalette[];
palindex : number = 0;
// TODO: control to select palette for bitmaps
constructor(context:EditorContext, fmt:PixelEditorImageFormat) {
super();
this.context = context;
this.ncolors = 1 << ((fmt.bpp||1) * (fmt.np||1));
}
updateLeft() {
if (this.right) { this.rgbimgs = this.right.rgbimgs; } // TODO: check is for unit test, remove?
var pal = new RGBAPalette(this.palette);
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() {
if (!this.updateRefs() && equalNestedArrays(this.images, this.left.images)) return false;
this.images = this.left.images;
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) => {
var out = new Uint32Array(im.length);
for (var i=0; i<im.length; i++) {
out[i] = this.palette[im[i] & mask];
}
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) {
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<cols.length; i++) {
var n = cols[i];
while (dup[n]) {
n ^= ++ndups;
}
res[i] = n;
dup[n] = 1;
}
return res;
}
export class PaletteFormatToRGB extends PixNode {
words : UintArray;
rgbimgs : Uint32Array[];
palette : Uint32Array;
palfmt : PixelEditorPaletteFormat;
layout : PixelEditorPaletteLayout;
constructor(palfmt) {
super();
this.palfmt = palfmt;
}
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];
this.rgbimgs = [];
this.palette.forEach( (rgba:number) => {
this.rgbimgs.push(new Uint32Array([rgba]));
});
return true;
}
getAllColors() {
var arr = [];
for (var i=0; i<getPaletteLength(this.palfmt); i++)
arr.push(i);
return convertPaletteFormat(arr, this.palfmt);
}
}
export abstract class Compositor extends PixNode {
tilemap : Uint8Array[]; // tilemap images
images : Uint8Array[]; // output (1 image)
width : number;
height : number;
context : EditorContext;
tileoptions : SelectableTilemap[];
tileindex : number = 0;
constructor(context:EditorContext) {
super();
this.context = context;
}
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);
}
}
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<this.rows; row++) {
for (var col=0; col<this.cols; col++) {
var name = this.words[this.baseofs + a];
var t = this.tilemap[name];
attraddr = (a & 0x2c00) | 0x3c0 | (a & 0x0C00) | ((a >> 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 = $('<div class="asset_grid"/>'); // 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 = $('<span/>');
agrid.append(span);
}
span.append(viewer.canvas);
var brk = (i % imgsperline) == imgsperline-1;
if (brk) {
agrid.append($("<br/>"));
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<string, number> = 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<this.palette.length; i++) {
var btn = $(document.createElement('button')).addClass('palbtn');
var rgb = this.palette[i] & 0xffffff;
var color = "#" + hex(rgb2bgr(rgb), 6);
btn.click(this.setPaletteColor.bind(this, i));
btn.css('backgroundColor', color).text(i.toString(16));
btn.css('color', (rgb & 0x008000) ? 'black' : 'white');
span.append(btn);
this.palbtns.push(btn);
}
return span;
}
commit() {
this.updateImage();
this.left.refreshLeft();
}
remapPixels(mapfn : (x:number,y:number) => number) {
var i = 0;
var pixels = new Uint32Array(this.rgbdata.length);
for (var y=0; y<this.height; y++) {
for (var x=0; x<this.width; x++) {
pixels[i] = mapfn(x, y);
i++;
}
}
this.rgbdata.set(pixels);
this.commit();
}
rotate(deg:number) {
var s1 = Math.sin(deg * Math.PI / 180);
var c1 = Math.cos(deg * Math.PI / 180);
this.remapPixels((x,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);
});
}
}