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};
|
2018-08-16 23:19:20 +00:00
|
|
|
|
2019-03-22 16:32:37 +00:00
|
|
|
// TODO: separate view/controller
|
2019-03-22 14:51:41 +00:00
|
|
|
export interface EditorContext {
|
2019-03-22 16:32:37 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2019-03-20 18:10:50 +00:00
|
|
|
export type PixelEditorImageFormat = {
|
2018-10-02 15:02:23 +00:00
|
|
|
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
|
2018-10-02 15:02:23 +00:00
|
|
|
};
|
|
|
|
|
2019-03-20 18:10:50 +00:00
|
|
|
export type PixelEditorPaletteFormat = {
|
2019-03-21 02:49:44 +00:00
|
|
|
pal?:number|string
|
2018-10-02 15:02:23 +00:00
|
|
|
n?:number
|
2019-03-21 02:49:44 +00:00
|
|
|
layout?:string
|
2018-10-02 15:02:23 +00:00
|
|
|
};
|
|
|
|
|
2019-03-21 16:13:27 +00:00
|
|
|
export type PixelEditorPaletteLayout = [string, number, number][];
|
|
|
|
|
2018-10-02 15:02:23 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-05-18 02:33:56 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2017-05-18 02:33:56 +00:00
|
|
|
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() {
|
2018-08-21 13:27:14 +00:00
|
|
|
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');
|
2017-05-05 13:51:47 +00:00
|
|
|
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];
|
|
|
|
}
|
2018-08-21 13:27:14 +00:00
|
|
|
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) {
|
2017-05-05 13:51:47 +00:00
|
|
|
var ofs = x+y*width;
|
2017-05-04 15:54:56 +00:00
|
|
|
return getPixelByOffset(ofs);
|
|
|
|
}
|
|
|
|
|
|
|
|
function setPixel(x, y, col) {
|
2017-05-05 13:51:47 +00:00
|
|
|
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;
|
2017-05-18 02:33:56 +00:00
|
|
|
commit();
|
2018-07-11 00:58:46 +00:00
|
|
|
// TODO: pixcanvas.releaseCapture();
|
2017-05-04 23:25:41 +00:00
|
|
|
});
|
|
|
|
}
|
2017-05-18 02:33:56 +00:00
|
|
|
|
2018-02-09 22:23:14 +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++]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-18 02:33:56 +00:00
|
|
|
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();
|
2017-05-18 02:33:56 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2018-02-09 22:23:14 +00:00
|
|
|
setPixels(p);
|
|
|
|
commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.flipy = function() {
|
|
|
|
console.log("flipy");
|
2018-12-07 15:03:01 +00:00
|
|
|
var p = this.getImageColors();
|
2018-02-09 22:23:14 +00:00
|
|
|
var i = 0;
|
2017-05-18 02:33:56 +00:00
|
|
|
for (var y=0; y<height; y++) {
|
|
|
|
for (var x=0; x<width; x++) {
|
2018-02-09 22:23:14 +00:00
|
|
|
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();
|
2018-02-09 22:23:14 +00:00
|
|
|
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;
|
2017-05-18 02:33:56 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-09 22:23:14 +00:00
|
|
|
setPixels(p);
|
2017-05-18 02:33:56 +00:00
|
|
|
commit();
|
|
|
|
}
|
2017-05-04 15:54:56 +00:00
|
|
|
}
|
|
|
|
|
2017-05-04 23:25:41 +00:00
|
|
|
/////////////////
|
|
|
|
|
2018-02-26 21:42:20 +00:00
|
|
|
var pixel_re = /([0#]?)([x$%]|\d'[bh])([0-9a-f]+)/gi;
|
2017-05-05 13:51:47 +00:00
|
|
|
|
2018-12-07 15:03:01 +00:00
|
|
|
function convertToHexStatements(s:string) : string {
|
2018-07-31 02:46:03 +00:00
|
|
|
// convert 'hex ....' asm format
|
2018-08-05 14:00:53 +00:00
|
|
|
return s.replace(/(\shex\s+)([0-9a-f]+)/ig, function(m,hexprefix,hexstr) {
|
2018-07-31 02:46:03 +00:00
|
|
|
var rtn = hexprefix;
|
|
|
|
for (var i=0; i<hexstr.length; i+=2) {
|
|
|
|
rtn += '0x'+hexstr.substr(i,2)+',';
|
|
|
|
}
|
|
|
|
return rtn;
|
|
|
|
});
|
2018-08-05 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-07 15:03:01 +00:00
|
|
|
export function parseHexWords(s:string) : number[] {
|
2018-08-05 14:00:53 +00:00
|
|
|
var arr = [];
|
|
|
|
var m;
|
2017-05-05 13:51:47 +00:00
|
|
|
while (m = pixel_re.exec(s)) {
|
|
|
|
var n;
|
2017-11-21 16:15:08 +00:00
|
|
|
if (m[2].startsWith('%') || m[2].endsWith("b"))
|
2017-05-05 13:51:47 +00:00
|
|
|
n = parseInt(m[3],2);
|
2018-02-26 21:42:20 +00:00
|
|
|
else if (m[2].startsWith('x') || m[2].startsWith('$') || m[2].endsWith('h'))
|
2017-05-05 13:51:47 +00:00
|
|
|
n = parseInt(m[3],16);
|
2017-05-04 15:54:56 +00:00
|
|
|
else
|
2017-05-05 13:51:47 +00:00
|
|
|
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;
|
2017-05-05 13:51:47 +00:00
|
|
|
while (m = pixel_re.exec(s)) {
|
|
|
|
result += s.slice(li, pixel_re.lastIndex - m[0].length);
|
|
|
|
li = pixel_re.lastIndex;
|
|
|
|
if (m[2].startsWith('%'))
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + "%" + words[i++].toString(2);
|
2017-11-21 16:15:08 +00:00
|
|
|
else if (m[2].endsWith('b'))
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + m[2] + words[i++].toString(2); // TODO
|
2018-02-26 21:42:20 +00:00
|
|
|
else if (m[2].endsWith('h'))
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + m[2] + words[i++].toString(16); // TODO
|
2017-05-05 13:51:47 +00:00
|
|
|
else if (m[2].startsWith('x'))
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + "x" + hex(words[i++]);
|
2017-05-05 13:51:47 +00:00
|
|
|
else if (m[2].startsWith('$'))
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + "$" + hex(words[i++]);
|
2017-05-04 15:54:56 +00:00
|
|
|
else
|
2018-10-02 15:02:23 +00:00
|
|
|
result += m[1] + words[i++].toString();
|
2017-05-04 15:54:56 +00:00
|
|
|
}
|
|
|
|
result += s.slice(li);
|
2018-07-31 02:46:03 +00:00
|
|
|
// convert 'hex ....' asm format
|
2018-12-07 15:03:01 +00:00
|
|
|
result = result.replace(/(\shex\s+)([,x0-9a-f]+)/ig, (m,hexprefix,hexstr) => {
|
2018-07-31 02:46:03 +00:00
|
|
|
var rtn = hexprefix + hexstr;
|
2018-08-05 14:00:53 +00:00
|
|
|
rtn = rtn.replace(/0x/ig,'').replace(/,/ig,'')
|
2018-07-31 02:46:03 +00:00
|
|
|
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 {
|
2017-05-08 11:58:45 +00:00
|
|
|
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;
|
2017-05-08 11:58:45 +00:00
|
|
|
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;
|
2018-10-02 15:02:23 +00:00
|
|
|
var wordsperline = fmt.sl || Math.ceil(width * bpp / bitsperword);
|
2017-05-04 23:25:41 +00:00
|
|
|
var mask = (1 << bpp)-1;
|
2018-10-02 15:02:23 +00:00
|
|
|
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++) {
|
2018-10-02 15:02:23 +00:00
|
|
|
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;
|
2017-05-08 11:58:45 +00:00
|
|
|
var ofs = remapBits(ofs0, fmt.remap);
|
2017-05-04 15:54:56 +00:00
|
|
|
for (var p=0; p<nplanes; p++) {
|
2018-10-02 15:02:23 +00:00
|
|
|
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) {
|
2017-05-08 11:58:45 +00:00
|
|
|
ofs0 += 1;
|
2017-05-04 15:54:56 +00:00
|
|
|
shift = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-05-25 19:49:30 +00:00
|
|
|
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[] {
|
2018-08-14 20:28:29 +00:00
|
|
|
if (fmt.destfmt) fmt = fmt.destfmt;
|
2017-05-04 15:54:56 +00:00
|
|
|
var width = fmt.w;
|
|
|
|
var height = fmt.h;
|
2017-05-08 11:58:45 +00:00
|
|
|
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;
|
2018-10-02 15:02:23 +00:00
|
|
|
var wordsperline = fmt.sl || Math.ceil(fmt.w * bpp / bitsperword);
|
2017-05-04 23:25:41 +00:00
|
|
|
var mask = (1 << bpp)-1;
|
2018-10-02 15:02:23 +00:00
|
|
|
var pofs = fmt.pofs || wordsperline*height*count;
|
|
|
|
var words;
|
2017-12-04 21:40:10 +00:00
|
|
|
if (bitsperword <= 8)
|
2018-10-02 15:02:23 +00:00
|
|
|
words = new Uint8Array(wordsperline*height*count*nplanes);
|
2017-12-04 21:40:10 +00:00
|
|
|
else
|
2018-10-02 15:02:23 +00:00
|
|
|
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++) {
|
2018-10-02 15:02:23 +00:00
|
|
|
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++];
|
2017-05-08 11:58:45 +00:00
|
|
|
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;
|
2018-10-02 15:02:23 +00:00
|
|
|
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) {
|
2017-05-08 11:58:45 +00:00
|
|
|
ofs0 += 1;
|
2017-05-04 15:54:56 +00:00
|
|
|
shift = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-10-02 15:02:23 +00:00
|
|
|
return words;
|
2017-05-04 15:54:56 +00:00
|
|
|
}
|
|
|
|
|
2019-03-20 18:10:50 +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;
|
2018-08-16 23:19:20 +00:00
|
|
|
export var paletteSets;
|
|
|
|
export var paletteSetIndex=0;
|
|
|
|
export var currentPixelEditor;
|
|
|
|
export var parentSource;
|
|
|
|
export var parentOrigin;
|
|
|
|
export var allimages;
|
2018-10-02 15:02:23 +00:00
|
|
|
export var currentFormat : PixelEditorImageFormat;
|
|
|
|
export var currentByteStr : string;
|
|
|
|
export var currentPaletteStr : string;
|
|
|
|
export var currentPaletteFmt : PixelEditorPaletteFormat;
|
2018-08-16 23:19:20 +00:00
|
|
|
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[] {
|
2019-03-20 18:10:50 +00:00
|
|
|
var pal = palfmt.pal;
|
|
|
|
var newpalette;
|
2019-03-21 02:49:44 +00:00
|
|
|
if (typeof pal === 'number') {
|
2019-03-20 18:10:50 +00:00
|
|
|
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)
|
2019-03-20 18:10:50 +00:00
|
|
|
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; });
|
2019-03-20 18:10:50 +00:00
|
|
|
} else {
|
|
|
|
throw new Error("No palette named " + pal);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newpalette;
|
|
|
|
}
|
|
|
|
|
2018-08-16 23:19:20 +00:00
|
|
|
export function pixelEditorDecodeMessage(e) {
|
2017-05-04 15:54:56 +00:00
|
|
|
parentSource = e.source;
|
|
|
|
parentOrigin = e.origin;
|
2018-10-02 15:02:23 +00:00
|
|
|
let data : PixelEditorMessage = e.data;
|
2017-05-04 15:54:56 +00:00
|
|
|
currentFormat = e.data.fmt;
|
2018-10-02 15:02:23 +00:00
|
|
|
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) {
|
2018-10-02 15:02:23 +00:00
|
|
|
var palbytes = parseHexWords(data.palstr);
|
2019-03-20 18:10:50 +00:00
|
|
|
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 {
|
2017-05-25 19:49:30 +00:00
|
|
|
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);
|
2017-05-05 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2018-08-21 13:27:14 +00:00
|
|
|
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
|
|
|
}
|
2017-05-05 13:51:47 +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">');
|
2018-10-11 15:33:09 +00:00
|
|
|
var thumb = new PixelEditor(span[0] as HTMLElement, currentFormat, palette, allimages[i]);
|
2018-08-21 13:27:14 +00:00
|
|
|
// 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());
|
|
|
|
}
|
2018-10-02 15:02:23 +00:00
|
|
|
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);
|
2017-05-18 02:33:56 +00:00
|
|
|
} else {
|
|
|
|
switch (e.keyCode) {
|
2018-08-13 03:29:05 +00:00
|
|
|
case 82: // 'R'
|
2017-05-18 02:33:56 +00:00
|
|
|
currentPixelEditor.rotate(-90);
|
|
|
|
break;
|
2018-08-13 03:29:05 +00:00
|
|
|
case 114: // 'r'
|
2017-05-18 02:33:56 +00:00
|
|
|
currentPixelEditor.rotate(90);
|
|
|
|
break;
|
2018-08-13 03:29:05 +00:00
|
|
|
case 84: // 'T'
|
2017-05-18 02:33:56 +00:00
|
|
|
currentPixelEditor.rotate(-45);
|
|
|
|
break;
|
2018-08-13 03:29:05 +00:00
|
|
|
case 116: // 't'
|
2017-05-18 02:33:56 +00:00
|
|
|
currentPixelEditor.rotate(45);
|
|
|
|
break;
|
2018-02-09 22:23:14 +00:00
|
|
|
}
|
2018-02-26 21:42:20 +00:00
|
|
|
switch (e.charCode) {
|
2018-02-09 22:23:14 +00:00
|
|
|
case 104:
|
|
|
|
currentPixelEditor.flipx();
|
|
|
|
break;
|
2019-03-18 18:39:02 +00:00
|
|
|
|
2018-02-09 22:23:14 +00:00
|
|
|
currentPixelEditor.flipy();
|
|
|
|
break;
|
2017-05-18 02:33:56 +00:00
|
|
|
default:
|
|
|
|
console.log(e);
|
|
|
|
break;
|
|
|
|
}
|
2017-05-04 15:54:56 +00:00
|
|
|
}
|
|
|
|
}
|
2017-05-21 21:34:57 +00:00
|
|
|
|
2019-03-22 16:32:37 +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-21 02:49:44 +00:00
|
|
|
],
|
|
|
|
};
|
|
|
|
|
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;
|
2019-03-22 16:32:37 +00:00
|
|
|
label : string;
|
2019-03-21 16:13:27 +00:00
|
|
|
words : UintArray;
|
2019-03-21 02:49:44 +00:00
|
|
|
}
|
|
|
|
|
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;
|
2019-03-22 16:32:37 +00:00
|
|
|
this.label = fileid;
|
2019-03-21 16:13:27 +00:00
|
|
|
this.words = data;
|
2019-03-18 18:39:02 +00:00
|
|
|
}
|
|
|
|
updateLeft() {
|
2019-03-21 02:49:44 +00:00
|
|
|
if (this.project) {
|
2019-03-21 16:13:27 +00:00
|
|
|
this.project.updateFile(this.fileid, this.words as Uint8Array);
|
2019-03-21 02:49:44 +00:00
|
|
|
}
|
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;
|
|
|
|
|
2019-03-22 16:32:37 +00:00
|
|
|
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;
|
2019-03-22 16:32:37 +00:00
|
|
|
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);
|
2019-03-21 02:49:44 +00:00
|
|
|
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-21 02:49:44 +00:00
|
|
|
}
|
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;
|
|
|
|
|
2019-03-22 16:32:37 +00:00
|
|
|
ncolors : number;
|
|
|
|
context : EditorContext;
|
2019-03-22 19:59:34 +00:00
|
|
|
paloptions : SelectablePalette[];
|
|
|
|
palindex : number = 0;
|
2019-03-22 16:32:37 +00:00
|
|
|
|
|
|
|
// 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() {
|
2019-03-22 19:59:34 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
2019-03-22 19:59:34 +00:00
|
|
|
updateRefs() {
|
2019-03-22 16:32:37 +00:00
|
|
|
if (this.context != null) {
|
2019-03-22 19:59:34 +00:00
|
|
|
this.paloptions = this.context.getPalettes(this.ncolors);
|
|
|
|
if (this.paloptions && this.paloptions.length > 0) {
|
|
|
|
this.palette = this.paloptions[this.palindex].palette;
|
2019-03-22 16:32:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
2019-03-21 02:49:44 +00:00
|
|
|
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-20 18:10:50 +00:00
|
|
|
|
2019-03-21 16:13:27 +00:00
|
|
|
words : UintArray;
|
|
|
|
rgbimgs : Uint32Array[];
|
2019-03-20 18:10:50 +00:00
|
|
|
palette : Uint32Array;
|
|
|
|
palfmt : PixelEditorPaletteFormat;
|
2019-03-21 16:13:27 +00:00
|
|
|
layout : PixelEditorPaletteLayout;
|
2019-03-20 18:10:50 +00:00
|
|
|
|
|
|
|
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 = [];
|
2019-03-20 18:10:50 +00:00
|
|
|
this.palette.forEach( (rgba:number) => {
|
2019-03-21 16:13:27 +00:00
|
|
|
this.rgbimgs.push(new Uint32Array([rgba]));
|
2019-03-20 18:10:50 +00:00
|
|
|
});
|
|
|
|
}
|
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);
|
|
|
|
}
|
2019-03-20 18:10:50 +00:00
|
|
|
}
|
|
|
|
|
2019-03-22 19:59:34 +00:00
|
|
|
export abstract class Compositor extends PixNode {
|
2019-03-22 16:32:37 +00:00
|
|
|
|
|
|
|
tilemap : Uint8Array[]; // tilemap images
|
2019-03-22 19:59:34 +00:00
|
|
|
images : Uint8Array[]; // output (1 image)
|
2019-03-22 16:32:37 +00:00
|
|
|
width : number;
|
|
|
|
height : number;
|
2019-03-22 19:59:34 +00:00
|
|
|
|
2019-03-22 16:32:37 +00:00
|
|
|
context : EditorContext;
|
|
|
|
tileoptions : SelectableTilemap[];
|
|
|
|
tileindex : number = 0;
|
|
|
|
|
|
|
|
constructor(context:EditorContext) {
|
|
|
|
super();
|
|
|
|
this.context = context;
|
|
|
|
}
|
2019-03-22 19:59:34 +00:00
|
|
|
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);
|
|
|
|
}
|
2019-03-22 16:32:37 +00:00
|
|
|
updateLeft() {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
updateRight() {
|
|
|
|
this.words = this.left.words;
|
2019-03-22 19:59:34 +00:00
|
|
|
this.updateRefs();
|
2019-03-22 16:32:37 +00:00
|
|
|
this.cols = 32;
|
|
|
|
this.rows = 30;
|
|
|
|
this.width = this.cols * 8;
|
|
|
|
this.height = this.rows * 8;
|
|
|
|
this.baseofs = 0;
|
2019-03-22 19:59:34 +00:00
|
|
|
var idata = new Uint8Array(this.width * this.height);
|
|
|
|
this.images = [idata];
|
2019-03-22 16:32:37 +00:00
|
|
|
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;
|
2019-03-22 19:59:34 +00:00
|
|
|
idata[i++] = color;
|
2019-03-22 16:32:37 +00:00
|
|
|
}
|
|
|
|
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) {
|
2019-03-20 18:10:50 +00:00
|
|
|
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");
|
2019-03-20 18:10:50 +00:00
|
|
|
this.ctx = c.getContext('2d');
|
2019-03-18 18:39:02 +00:00
|
|
|
return c;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateImage(imdata : Uint32Array) {
|
2019-03-20 18:10:50 +00:00
|
|
|
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
|
2019-03-22 16:32:37 +00:00
|
|
|
// 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));
|
|
|
|
});
|
2019-03-22 16:32:37 +00:00
|
|
|
// add palette selector
|
|
|
|
// TODO: only view when editing?
|
|
|
|
var palizer = this.left;
|
2019-03-22 19:59:34 +00:00
|
|
|
if (palizer instanceof Palettizer && palizer.paloptions.length > 1) {
|
2019-03-22 16:32:37 +00:00
|
|
|
var palselect = $(document.createElement('select'));
|
2019-03-22 19:59:34 +00:00
|
|
|
palizer.paloptions.forEach((palopt, i) => {
|
2019-03-22 16:32:37 +00:00
|
|
|
// TODO: full identifier
|
|
|
|
var sel = $(document.createElement('option')).text(palopt.name).val(i).appendTo(palselect);
|
2019-03-22 19:59:34 +00:00
|
|
|
if (i == (palizer as Palettizer).palindex)
|
2019-03-22 16:32:37 +00:00
|
|
|
sel.attr('selected','selected');
|
|
|
|
});
|
|
|
|
palselect.appendTo(agrid).change((e) => {
|
|
|
|
var index = $(e.target).val() as number;
|
2019-03-22 19:59:34 +00:00
|
|
|
(palizer as Palettizer).palindex = index;
|
2019-03-22 16:32:37 +00:00
|
|
|
palizer.refreshRight();
|
|
|
|
});
|
|
|
|
}
|
2019-03-22 14:51:41 +00:00
|
|
|
}
|
|
|
|
|
2019-03-21 16:13:27 +00:00
|
|
|
}
|
|
|
|
|