1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-09-27 08:54:48 +00:00
8bitworkshop/src/pixed/pixeleditor.ts

1120 lines
30 KiB
TypeScript
Raw Normal View History

2017-05-04 15:54:56 +00:00
"use strict";
2019-03-21 16:13:27 +00:00
import { hex, rgb2bgr, rle_unpack } from "../util";
import { ProjectWindows } from "../windows";
export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i:number]:number};
// TODO: separate view/controller
2019-03-22 14:51:41 +00:00
export interface EditorContext {
setCurrentEditor(div:JQuery, editing:JQuery) : 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?
2019-03-22 14:51:41 +00:00
}
export type PixelEditorImageFormat = {
w:number
h:number
count?:number
bpp?:number
np?:number
bpw?:number
sl?:number
pofs?:number
remap?:number[]
brev?:boolean
destfmt?:PixelEditorImageFormat
2019-03-18 18:39:02 +00:00
xform?:string
};
export type PixelEditorPaletteFormat = {
pal?:number|string
n?:number
layout?:string
};
2019-03-21 16:13:27 +00:00
export type PixelEditorPaletteLayout = [string, number, number][];
type PixelEditorMessage = {
fmt : PixelEditorImageFormat
palfmt : PixelEditorPaletteFormat
bytestr : string
palstr : string
};
2018-12-07 15:03:01 +00:00
export function PixelEditor(parentDiv:HTMLElement,
fmt:PixelEditorImageFormat,
palette:Uint32Array,
initialData:Uint32Array,
thumbnails?) {
2017-05-04 23:25:41 +00:00
var width = fmt.w;
var height = fmt.h;
2018-07-11 00:58:46 +00:00
function createCanvas() {
2017-05-04 15:54:56 +00:00
var c = document.createElement('canvas');
c.width = width;
c.height = height;
2017-05-04 23:25:41 +00:00
if (fmt.xform) c.style.transform = fmt.xform;
2017-05-04 15:54:56 +00:00
c.classList.add("pixels");
c.classList.add("pixelated");
//canvas.tabIndex = "-1"; // Make it focusable
$(parentDiv).empty().append(c);
return c;
}
function updateImage() {
ctx.putImageData(pixdata, 0, 0);
}
function commit() {
2017-05-04 23:25:41 +00:00
if (!thumbnails) return;
for (var i=0; i<thumbnails.length; i++) {
2018-12-07 15:03:01 +00:00
thumbnails[i].copyImageFrom(this);
2017-05-04 23:25:41 +00:00
}
2018-12-07 15:03:01 +00:00
initialData.set(this.getImageColors());
2017-05-04 23:25:41 +00:00
}
this.copyImageFrom = function(src) {
pixints.set(src.getImageData());
updateImage();
}
this.getImageData = function() { return pixints.slice(0); }
2017-05-04 23:25:41 +00:00
2017-05-04 15:54:56 +00:00
function fitCanvas() {
pixcanvas.style.height = '50%'; // TODO?
return;
2017-05-04 15:54:56 +00:00
}
this.resize = fitCanvas;
var pixcanvas = createCanvas();
var ctx = pixcanvas.getContext('2d');
var pixdata = ctx.createImageData(width, height);
2017-05-04 15:54:56 +00:00
var pixints = new Uint32Array(pixdata.data.buffer);
for (var i=0; i<pixints.length; i++) {
pixints[i] = initialData ? palette[initialData[i]] : palette[0];
}
this.canvas = pixcanvas;
2017-05-04 15:54:56 +00:00
updateImage();
2017-05-04 23:25:41 +00:00
this.createPaletteButtons = function() {
2017-05-04 15:54:56 +00:00
var span = $("#palette_group").empty();
for (var i=0; i<palette.length; i++) {
var btn = $('<button class="palbtn">');
var rgb = palette[i] & 0xffffff;
2019-03-21 16:13:27 +00:00
var color = "#" + hex(rgb2bgr(rgb), 6);
2018-12-07 15:03:01 +00:00
btn.click(this.setCurrentColor.bind(this, i));
2017-05-04 15:54:56 +00:00
btn.attr('id', 'palcol_' + i);
btn.css('backgroundColor', color).text(i.toString(16));
if ((rgb & 0x808080) != 0x808080) { btn.css('color', 'white'); }
span.append(btn);
}
2018-12-07 15:03:01 +00:00
this.setCurrentColor(1);
2017-05-04 15:54:56 +00:00
}
function getPixelByOffset(ofs) {
2017-05-04 23:25:41 +00:00
var oldrgba = pixints[ofs] & 0xffffff;
2017-05-04 15:54:56 +00:00
for (var i=0; i<palette.length; i++) {
2017-05-04 23:25:41 +00:00
if (oldrgba == (palette[i] & 0xffffff)) return i;
2017-05-04 15:54:56 +00:00
}
return 0;
}
function getPixel(x, y) {
var ofs = x+y*width;
2017-05-04 15:54:56 +00:00
return getPixelByOffset(ofs);
}
function setPixel(x, y, col) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
var ofs = x+y*width;
2017-05-04 15:54:56 +00:00
var oldrgba = pixints[ofs];
var rgba = palette[col];
if (oldrgba != rgba) {
pixints[ofs] = rgba;
updateImage();
}
}
this.getImageColors = function() {
var pixcols = new Uint8Array(pixints.length);
for (var i=0; i<pixints.length; i++)
pixcols[i] = getPixelByOffset(i);
return pixcols;
}
2017-05-04 23:25:41 +00:00
///
this.makeEditable = function() {
var curpalcol = -1;
setCurrentColor(1);
function getPositionFromEvent(e) {
var x = Math.floor(e.offsetX * width / pxls.width());
var y = Math.floor(e.offsetY * height / pxls.height());
return {x:x, y:y};
}
function setCurrentColor(col) {
if (curpalcol != col) {
if (curpalcol >= 0)
$("#palcol_" + curpalcol).removeClass('selected');
curpalcol = col;
$("#palcol_" + col).addClass('selected');
}
}
2018-12-07 15:03:01 +00:00
this.setCurrentColor = setCurrentColor;
2017-05-04 23:25:41 +00:00
var dragcol = 1;
var dragging = false;
var pxls = $(pixcanvas);
2018-12-07 15:03:01 +00:00
pxls.mousedown( (e) => {
2017-05-04 23:25:41 +00:00
var pos = getPositionFromEvent(e);
dragcol = getPixel(pos.x, pos.y) == curpalcol ? 0 : curpalcol;
setPixel(pos.x, pos.y, curpalcol);
dragging = true;
2018-07-11 00:58:46 +00:00
// TODO: pixcanvas.setCapture();
2017-05-04 23:25:41 +00:00
})
2018-12-07 15:03:01 +00:00
.mousemove( (e) => {
2017-05-04 23:25:41 +00:00
var pos = getPositionFromEvent(e);
if (dragging) {
setPixel(pos.x, pos.y, dragcol);
}
})
2018-12-07 15:03:01 +00:00
.mouseup( (e) => {
2017-05-04 23:25:41 +00:00
var pos = getPositionFromEvent(e);
setPixel(pos.x, pos.y, dragcol);
dragging = false;
commit();
2018-07-11 00:58:46 +00:00
// TODO: pixcanvas.releaseCapture();
2017-05-04 23:25:41 +00:00
});
}
function setPixels(p) {
var i = 0;
for (var y=0; y<height; y++) {
for (var x=0; x<width; x++) {
setPixel(x, y, p[i++]);
}
}
}
this.rotate = function(deg) {
console.log("rotate " + deg);
var s1 = Math.sin(deg * Math.PI / 180);
var c1 = Math.cos(deg * Math.PI / 180);
2018-12-07 15:03:01 +00:00
var p = this.getImageColors();
var i = 0;
for (var y=0; y<height; y++) {
for (var x=0; x<width; x++) {
var xx = x + 0.5 - width/2.0;
var yy = y + 0.5 - height/2.0;
var xx2 = xx*c1 - yy*s1 + width/2.0 - 0.5;
var yy2 = yy*c1 + xx*s1 + height/2.0 - 0.5;
var col = getPixel(Math.round(xx2), Math.round(yy2));
p[i++] = col;
}
}
setPixels(p);
commit();
}
this.flipy = function() {
console.log("flipy");
2018-12-07 15:03:01 +00:00
var p = this.getImageColors();
var i = 0;
for (var y=0; y<height; y++) {
for (var x=0; x<width; x++) {
var col = getPixel(x, height-1-y);
p[i++] = col;
}
}
setPixels(p);
commit();
}
this.flipx = function() {
console.log("flipx");
2018-12-07 15:03:01 +00:00
var p = this.getImageColors();
var i = 0;
for (var y=0; y<height; y++) {
for (var x=0; x<width; x++) {
var col = getPixel(width-1-x, y);
p[i++] = col;
}
}
setPixels(p);
commit();
}
2017-05-04 15:54:56 +00:00
}
2017-05-04 23:25:41 +00:00
/////////////////
var pixel_re = /([0#]?)([x$%]|\d'[bh])([0-9a-f]+)/gi;
2018-12-07 15:03:01 +00:00
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;
});
}
2018-12-07 15:03:01 +00:00
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);
2017-05-04 15:54:56 +00:00
else
n = parseInt(m[3]);
arr.push(n);
2017-05-04 15:54:56 +00:00
}
return arr;
}
2019-03-21 16:13:27 +00:00
export function replaceHexWords(s:string, words:UintArray) : string {
2017-05-04 15:54:56 +00:00
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++]);
2017-05-04 15:54:56 +00:00
else
result += m[1] + words[i++].toString();
2017-05-04 15:54:56 +00:00
}
result += s.slice(li);
// convert 'hex ....' asm format
2018-12-07 15:03:01 +00:00
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;
});
2017-05-04 15:54:56 +00:00
return result;
}
2018-12-07 15:03:01 +00:00
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;
}
2019-03-21 16:13:27 +00:00
function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] {
2017-05-04 15:54:56 +00:00
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || 1;
var bpp = fmt.bpp || 1;
var nplanes = fmt.np || 1;
2017-12-04 21:40:10 +00:00
var bitsperword = fmt.bpw || 8;
var wordsperline = fmt.sl || Math.ceil(width * bpp / bitsperword);
2017-05-04 23:25:41 +00:00
var mask = (1 << bpp)-1;
var pofs = fmt.pofs || wordsperline*height*count;
2017-05-04 15:54:56 +00:00
var images = [];
for (var n=0; n<count; n++) {
var imgdata = [];
for (var y=0; y<height; y++) {
var ofs0 = n*wordsperline*height + y*wordsperline;
2017-05-04 15:54:56 +00:00
var shift = 0;
for (var x=0; x<width; x++) {
var color = 0;
var ofs = remapBits(ofs0, fmt.remap);
2017-05-04 15:54:56 +00:00
for (var p=0; p<nplanes; p++) {
var byte = words[ofs + p*pofs];
2017-12-04 21:40:10 +00:00
color |= ((fmt.brev ? byte>>(bitsperword-shift-bpp) : byte>>shift) & mask) << (p*bpp);
2017-05-04 15:54:56 +00:00
}
imgdata.push(color);
2017-05-04 23:25:41 +00:00
shift += bpp;
2017-12-04 21:40:10 +00:00
if (shift >= bitsperword) {
ofs0 += 1;
2017-05-04 15:54:56 +00:00
shift = 0;
}
}
}
images.push(new Uint8Array(imgdata));
2017-05-04 15:54:56 +00:00
}
return images;
}
2018-12-07 15:03:01 +00:00
function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : number[] {
if (fmt.destfmt) fmt = fmt.destfmt;
2017-05-04 15:54:56 +00:00
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || 1;
var bpp = fmt.bpp || 1;
var nplanes = fmt.np || 1;
2017-12-04 21:40:10 +00:00
var bitsperword = fmt.bpw || 8;
var wordsperline = fmt.sl || Math.ceil(fmt.w * bpp / bitsperword);
2017-05-04 23:25:41 +00:00
var mask = (1 << bpp)-1;
var pofs = fmt.pofs || wordsperline*height*count;
var words;
2017-12-04 21:40:10 +00:00
if (bitsperword <= 8)
words = new Uint8Array(wordsperline*height*count*nplanes);
2017-12-04 21:40:10 +00:00
else
words = new Uint32Array(wordsperline*height*count*nplanes);
2017-05-04 15:54:56 +00:00
for (var n=0; n<count; n++) {
var imgdata = images[n];
2017-05-04 23:25:41 +00:00
var i = 0;
2017-05-04 15:54:56 +00:00
for (var y=0; y<height; y++) {
var ofs0 = n*wordsperline*height + y*wordsperline;
2017-05-04 15:54:56 +00:00
var shift = 0;
for (var x=0; x<width; x++) {
var color = imgdata[i++];
var ofs = remapBits(ofs0, fmt.remap);
2017-05-04 15:54:56 +00:00
for (var p=0; p<nplanes; p++) {
2017-05-04 23:25:41 +00:00
var c = (color >> (p*bpp)) & mask;
words[ofs + p*pofs] |= (fmt.brev ? (c << (bitsperword-shift-bpp)) : (c << shift));
2017-05-04 15:54:56 +00:00
}
2017-05-04 23:25:41 +00:00
shift += bpp;
2017-12-04 21:40:10 +00:00
if (shift >= bitsperword) {
ofs0 += 1;
2017-05-04 15:54:56 +00:00
shift = 0;
}
}
}
}
return words;
2017-05-04 15:54:56 +00:00
}
// TODO
2019-03-21 16:13:27 +00:00
function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] {
2017-05-04 15:54:56 +00:00
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;
}
2018-12-07 15:03:01 +00:00
export var palette : Uint32Array;
export var paletteSets;
export var paletteSetIndex=0;
export var currentPixelEditor;
export var parentSource;
export var parentOrigin;
export var allimages;
export var currentFormat : PixelEditorImageFormat;
export var currentByteStr : string;
export var currentPaletteStr : string;
export var currentPaletteFmt : PixelEditorPaletteFormat;
export var allthumbs;
2019-03-21 16:13:27 +00:00
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
2019-03-21 16:13:27 +00:00
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) {
2019-03-21 16:13:27 +00:00
newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length-1)] | 0xff000000; });
} else {
throw new Error("No palette named " + pal);
}
}
return newpalette;
}
export function pixelEditorDecodeMessage(e) {
2017-05-04 15:54:56 +00:00
parentSource = e.source;
parentOrigin = e.origin;
let data : PixelEditorMessage = e.data;
2017-05-04 15:54:56 +00:00
currentFormat = e.data.fmt;
currentPaletteFmt = data.palfmt;
currentPaletteStr = data.palstr;
currentByteStr = convertToHexStatements(data.bytestr);
var words = parseHexWords(currentByteStr);
allimages = convertWordsToImages(words, data.fmt);
2018-12-07 15:03:01 +00:00
var newpalette = [0xff000000, 0xffffffff]; // TODO
2017-05-04 15:54:56 +00:00
if (currentPaletteStr) {
var palbytes = parseHexWords(data.palstr);
newpalette = convertPaletteFormat(palbytes, currentPaletteFmt) || newpalette;
2017-05-10 02:43:42 +00:00
if (currentPaletteFmt.n) {
paletteSets = [];
2018-12-07 15:03:01 +00:00
for (var i=0; i<newpalette.length; i+=currentPaletteFmt.n) {
paletteSets.push(newpalette.slice(i, i+currentPaletteFmt.n));
2017-05-10 02:43:42 +00:00
}
2018-12-07 15:03:01 +00:00
newpalette = paletteSets[paletteSetIndex = 0];
2017-05-10 02:43:42 +00:00
// TODO: swap palettes
}
2017-05-04 23:25:41 +00:00
} else {
var ncols = (currentFormat.bpp || 1) * (currentFormat.np || 1);
2017-05-21 21:34:57 +00:00
switch (ncols) {
case 2:
2018-12-07 15:03:01 +00:00
newpalette = [0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff];
2017-05-21 21:34:57 +00:00
break;
// TODO
}
2017-05-04 23:25:41 +00:00
// TODO: default palette?
}
2018-12-07 15:03:01 +00:00
palette = new Uint32Array(newpalette);
}
function pixelEditorCreateThumbnails(e) {
2017-05-04 23:25:41 +00:00
// create thumbnail for all images
$("#thumbnaildiv").empty();
var parentdiv;
var count = e.data.fmt.count || 1;
allthumbs = [];
for (var i=0; i<count; i++) {
if ((i & 15) == 0) {
parentdiv = $('<div class="thumbdiv">').appendTo("#thumbnaildiv");
2017-05-04 23:25:41 +00:00
}
allthumbs.push(createThumbnailForImage(parentdiv, i));
2017-05-04 15:54:56 +00:00
}
}
function pixelEditorReceiveMessage(e) {
pixelEditorDecodeMessage(e);
pixelEditorCreateThumbnails(e);
2017-05-04 23:25:41 +00:00
// create initial editor
createEditorForImage(0);
}
function createThumbnailForImage(parentdiv, i) {
var span = $('<span class="thumb">');
var thumb = new PixelEditor(span[0] as HTMLElement, currentFormat, palette, allimages[i]);
// double size of canvas thumbnail
thumb.canvas.style.height = currentFormat.h*2+"px";
thumb.canvas.style.width = currentFormat.w*2+"px";
2017-05-04 23:25:41 +00:00
parentdiv.append(span);
2018-12-07 15:03:01 +00:00
span.click(() => { createEditorForImage(i) });
2017-05-04 23:25:41 +00:00
return thumb;
}
function createEditorForImage(i) {
2018-07-11 00:58:46 +00:00
currentPixelEditor = new PixelEditor(document.getElementById('maineditor'), currentFormat, palette, allimages[i], [allthumbs[i]]);
2017-05-04 23:25:41 +00:00
currentPixelEditor.resize();
currentPixelEditor.makeEditable();
currentPixelEditor.createPaletteButtons();
2017-05-04 15:54:56 +00:00
}
function postToParentWindow(data) {
if (data.save) {
2017-05-04 23:25:41 +00:00
var allimgs = [];
for (var i=0; i<allthumbs.length; i++) {
allimgs.push(allthumbs[i].getImageColors());
}
data.bytes = convertImagesToWords(allimgs, currentFormat);
data.bytestr = replaceHexWords(currentByteStr, data.bytes);
2017-05-04 15:54:56 +00:00
}
2017-05-04 23:25:41 +00:00
if (parentSource) parentSource.postMessage(data, "*");
return data;
2017-05-04 15:54:56 +00:00
}
function pixelEditorResize(e) {
if (currentPixelEditor) {
currentPixelEditor.resize();
}
}
function pixelEditorKeypress(e) {
if (!currentPixelEditor) return;
2018-08-13 03:29:05 +00:00
//console.log(e);
2017-05-04 15:54:56 +00:00
var c = e.charCode;
if (c >= 48 && c <= 57) {
currentPixelEditor.setCurrentColor(c-48);
} else if (c >= 97 && c <= 102) {
currentPixelEditor.setCurrentColor(c-97+10);
} else {
switch (e.keyCode) {
2018-08-13 03:29:05 +00:00
case 82: // 'R'
currentPixelEditor.rotate(-90);
break;
2018-08-13 03:29:05 +00:00
case 114: // 'r'
currentPixelEditor.rotate(90);
break;
2018-08-13 03:29:05 +00:00
case 84: // 'T'
currentPixelEditor.rotate(-45);
break;
2018-08-13 03:29:05 +00:00
case 116: // 't'
currentPixelEditor.rotate(45);
break;
}
switch (e.charCode) {
case 104:
currentPixelEditor.flipx();
break;
2019-03-18 18:39:02 +00:00
currentPixelEditor.flipy();
break;
default:
console.log(e);
break;
}
2017-05-04 15:54:56 +00:00
}
}
2017-05-21 21:34:57 +00:00
// TODO: illegal colors?
2017-05-21 21:34:57 +00:00
var PREDEF_PALETTES = {
'nes':[
2019-03-21 16:13:27 +00:00
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
2017-05-21 21:34:57 +00:00
]
};
2019-03-18 18:39:02 +00:00
2019-03-21 16:13:27 +00:00
var PREDEF_LAYOUTS : {[id:string]:PixelEditorPaletteLayout} = {
'nes':[
['Screen Color', 0x00, 1],
['Background 1', 0x01, 3],
['Background 2', 0x05, 3],
['Background 3', 0x09, 3],
['Background 4', 0x0d, 3],
['Sprite 1', 0x11, 3],
['Sprite 2', 0x15, 3],
['Sprite 3', 0x19, 3],
['Sprite 4', 0x1d, 3]
],
};
2019-03-18 18:39:02 +00:00
/////
2019-03-22 14:51:41 +00:00
export abstract class PixNode {
left : PixNode; // toward text editor
right : PixNode; // toward pixel editor
2019-03-21 16:13:27 +00:00
words? : UintArray; // file data
images? : Uint8Array[]; // array of indexed image data
rgbimgs? : Uint32Array[]; // array of rgba imgages
2019-03-18 18:39:02 +00:00
abstract updateLeft(); // update coming from right
abstract updateRight(); // update coming from left
refreshLeft() {
2019-03-22 14:51:41 +00:00
var p : PixNode = this;
2019-03-18 18:39:02 +00:00
while (p) {
p.updateLeft();
p = p.left;
}
}
refreshRight() {
2019-03-22 14:51:41 +00:00
var p : PixNode = this;
2019-03-18 18:39:02 +00:00
while (p) {
p.updateRight();
p = p.right;
}
}
2019-03-22 14:51:41 +00:00
addRight(node : PixNode) {
2019-03-18 18:39:02 +00:00
this.right = node;
node.left = this;
2019-03-22 14:51:41 +00:00
return node;
}
addLeft(node : PixNode) {
this.left = node;
node.right = this;
return node;
2019-03-18 18:39:02 +00:00
}
}
2019-03-22 14:51:41 +00:00
abstract class CodeProjectDataNode extends PixNode {
2019-03-21 16:13:27 +00:00
project : ProjectWindows;
2019-03-18 18:39:02 +00:00
fileid : string;
label : string;
2019-03-21 16:13:27 +00:00
words : UintArray;
}
2019-03-21 16:13:27 +00:00
export class FileDataNode extends CodeProjectDataNode {
2019-03-18 18:39:02 +00:00
2019-03-21 16:13:27 +00:00
constructor(project:ProjectWindows, fileid:string, data:Uint8Array) {
2019-03-18 18:39:02 +00:00
super();
2019-03-21 16:13:27 +00:00
this.project = project;
2019-03-18 18:39:02 +00:00
this.fileid = fileid;
this.label = fileid;
2019-03-21 16:13:27 +00:00
this.words = data;
2019-03-18 18:39:02 +00:00
}
updateLeft() {
if (this.project) {
2019-03-21 16:13:27 +00:00
this.project.updateFile(this.fileid, this.words as Uint8Array);
}
2019-03-18 18:39:02 +00:00
}
updateRight() {
}
}
2019-03-21 16:13:27 +00:00
export class TextDataNode extends CodeProjectDataNode {
2019-03-18 18:39:02 +00:00
text : string;
start : number;
end : number;
constructor(project:ProjectWindows, fileid:string, label:string, text:string, start:number, end:number) {
2019-03-18 18:39:02 +00:00
super();
2019-03-21 16:13:27 +00:00
this.project = project;
2019-03-18 18:39:02 +00:00
this.fileid = fileid;
this.label = label;
2019-03-18 18:39:02 +00:00
this.text = text;
this.start = start;
this.end = end;
}
updateLeft() {
// TODO: reload editors?
2019-03-21 16:13:27 +00:00
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);
2019-03-21 16:13:27 +00:00
//this.project.replaceTextRange(this.fileid, this.start, this.end, datastr);
}
2019-03-18 18:39:02 +00:00
}
updateRight() {
var datastr = this.text.substring(this.start, this.end);
2019-03-21 00:45:03 +00:00
datastr = convertToHexStatements(datastr); // TODO?
2019-03-18 18:39:02 +00:00
var words = parseHexWords(datastr);
2019-03-21 16:13:27 +00:00
this.words = words; //new Uint8Array(words); // TODO: 16/32?
}
}
2019-03-22 14:51:41 +00:00
export class Compressor extends PixNode {
2019-03-21 16:13:27 +00:00
words : UintArray;
updateLeft() {
2019-03-22 14:51:41 +00:00
// TODO: can't modify length of rle bytes
2019-03-21 16:13:27 +00:00
}
updateRight() {
this.words = rle_unpack(new Uint8Array(this.left.words));
2019-03-18 18:39:02 +00:00
}
2019-03-21 16:13:27 +00:00
2019-03-18 18:39:02 +00:00
}
2019-03-22 14:51:41 +00:00
export class Mapper extends PixNode {
2019-03-18 18:39:02 +00:00
fmt : PixelEditorImageFormat;
2019-03-21 16:13:27 +00:00
words : UintArray;
images : Uint8Array[];
2019-03-18 18:39:02 +00:00
updateLeft() {
2019-03-21 16:13:27 +00:00
this.images = this.right.images;
this.words = convertImagesToWords(this.images, this.fmt);
2019-03-18 18:39:02 +00:00
}
updateRight() {
// convert each word array to images
2019-03-21 16:13:27 +00:00
this.words = this.left.words;
this.images = convertWordsToImages(this.words, this.fmt);
2019-03-18 18:39:02 +00:00
}
2019-03-21 16:13:27 +00:00
}
2019-03-18 18:39:02 +00:00
2019-03-21 16:13:27 +00:00
class RGBAPalette {
palcols;
constructor(palcols : Uint32Array) {
this.palcols = palcols;
}
indexOf(rgba : number) : number {
return this.palcols.find(rgba);
}
2019-03-18 18:39:02 +00:00
}
2019-03-22 14:51:41 +00:00
export class Palettizer extends PixNode {
2019-03-18 18:39:02 +00:00
2019-03-21 16:13:27 +00:00
images : Uint8Array[];
rgbimgs : Uint32Array[];
2019-03-18 18:39:02 +00:00
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));
}
2019-03-18 18:39:02 +00:00
updateLeft() {
2019-03-21 16:13:27 +00:00
this.rgbimgs = this.right.rgbimgs;
var pal = new RGBAPalette(this.palette);
this.images = 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;
});
2019-03-18 18:39:02 +00:00
}
updateRight() {
this.updateRefs();
2019-03-21 16:13:27 +00:00
this.images = this.left.images;
2019-03-18 18:39:02 +00:00
var mask = this.palette.length - 1; // must be power of 2
// for each image, map bytes to RGB colors
2019-03-21 16:13:27 +00:00
this.rgbimgs = this.images.map( (im:Uint8Array) => {
2019-03-18 18:39:02 +00:00
var out = new Uint32Array(im.length);
for (var i=0; i<im.length; i++) {
out[i] = this.palette[im[i] & mask];
}
return out;
});
}
updateRefs() {
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;
}
}
if (this.palette == null) {
if (this.ncolors <= 2)
this.palette = new Uint32Array([0xff000000, 0xffffffff]);
else
this.palette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO: more palettes
}
}
2019-03-18 18:39:02 +00:00
}
2019-03-21 16:13:27 +00:00
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;
}
2019-03-22 14:51:41 +00:00
export class PaletteFormatToRGB extends PixNode {
2019-03-21 16:13:27 +00:00
words : UintArray;
rgbimgs : Uint32Array[];
palette : Uint32Array;
palfmt : PixelEditorPaletteFormat;
2019-03-21 16:13:27 +00:00
layout : PixelEditorPaletteLayout;
updateLeft() {
//TODO
}
updateRight() {
2019-03-21 16:13:27 +00:00
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) => {
2019-03-21 16:13:27 +00:00
this.rgbimgs.push(new Uint32Array([rgba]));
});
}
2019-03-21 16:13:27 +00:00
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() {
if (this.context != null) {
this.tileoptions = this.context.getTilemaps(256);
if (this.tileoptions && this.tileoptions.length > 0) {
this.tilemap = this.tileoptions[this.tileindex].images;
}
}
}
}
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
}
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
});
}
}
export class NESNametableConverter extends Compositor {
cols : number;
rows : number;
baseofs : number;
constructor(context:EditorContext) {
super(context);
}
updateLeft() {
// TODO
}
updateRight() {
this.words = this.left.words;
this.updateRefs();
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
}
}
///// UI CONTROLS
2019-03-22 14:51:41 +00:00
export class Viewer { // TODO: make PixNode
2019-03-18 18:39:02 +00:00
width : number;
height : number;
canvas : HTMLCanvasElement;
ctx : CanvasRenderingContext2D;
pixdata : ImageData;
recreate() {
this.canvas = this.newCanvas();
this.pixdata = this.ctx.createImageData(this.width, this.height);
}
2019-03-21 16:13:27 +00:00
createWith(pv : Viewer) {
this.width = pv.width;
this.height = pv.height;
this.pixdata = pv.pixdata;
this.canvas = this.newCanvas();
}
2019-03-18 18:39:02 +00:00
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');
2019-03-18 18:39:02 +00:00
return c;
}
updateImage(imdata : Uint32Array) {
if (imdata) {
new Uint32Array(this.pixdata.data.buffer).set(imdata);
}
2019-03-18 18:39:02 +00:00
this.ctx.putImageData(this.pixdata, 0, 0);
}
}
2019-03-21 16:13:27 +00:00
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.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;
}
});
2019-03-22 14:51:41 +00:00
}
}
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;
constructor(context:EditorContext, parentdiv:JQuery, fmt:PixelEditorImageFormat) {
super();
this.context = context;
this.parentdiv = parentdiv;
this.fmt = fmt;
2019-03-21 16:13:27 +00:00
}
2019-03-22 14:51:41 +00:00
updateLeft() { } // TODO
updateRight() {
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
2019-03-22 14:51:41 +00:00
var 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 = new Viewer();
editview.createWith(viewer);
editview.updateImage(null);
editview.canvas.style.width = (viewer.width*escale)+'px'; // TODO
aeditor.empty().append(editview.canvas);
this.context.setCurrentEditor(aeditor, $(viewer.canvas));
});
// 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();
});
}
2019-03-22 14:51:41 +00:00
}
2019-03-21 16:13:27 +00:00
}