Files
8bitworkshop/gen/ide/pixeleditor.js
T

1808 lines
69 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwoWayMapper = exports.Viewer = exports.MapEditor = exports.CharmapEditor = exports.PaletteColorPicker = exports.ImageChooser = exports.NESNametableConverter = exports.MetaspriteCompositor = exports.Compositor = exports.PaletteEditorView = exports.PaletteFormatToRGB = exports.Palettizer = exports.Mapper = exports.Compressor = exports.TextDataNode = exports.FileDataNode = exports.PixNode = exports.PREDEF_LAYOUTS = void 0;
exports.disambiguatePaletteNames = disambiguatePaletteNames;
exports.parseHexWords = parseHexWords;
exports.replaceHexWords = replaceHexWords;
exports.convertWordsToImages = convertWordsToImages;
exports.validateAssetData = validateAssetData;
exports.validateAssetByteLength = validateAssetByteLength;
exports.scanTextForAssetFragments = scanTextForAssetFragments;
exports.resolveEmbedPath = resolveEmbedPath;
exports.convertImagesToWords = convertImagesToWords;
exports.convertPaletteBytes = convertPaletteBytes;
exports.decodePacmanColorPromByte = decodePacmanColorPromByte;
exports.getPaletteLength = getPaletteLength;
exports.convertPaletteFormat = convertPaletteFormat;
exports.getDirectColorChannels = getDirectColorChannels;
exports.encodeDirectColorWord = encodeDirectColorWord;
exports.decodeDirectColorWord = decodeDirectColorWord;
exports.computePaletteSlices = computePaletteSlices;
exports.getPixelClipboard = getPixelClipboard;
exports.setPixelClipboard = setPixelClipboard;
exports.pasteClipboardPixels = pasteClipboardPixels;
const util_1 = require("../common/util");
const toolbar_1 = require("./toolbar");
const MAX_SIZE_X = 800;
const MAX_SIZE_Y = 1000;
const MAX_SCALE = 16;
// Layout slice names repeat when a project has several palettes (e.g. many NES
// palettes all produce "Background 0"). Prefix just the duplicate names with
// their owning palette's `group` so the dropdown stays unambiguous.
function disambiguatePaletteNames(palettes) {
var counts = {};
palettes.forEach((p) => { counts[p.name] = (counts[p.name] || 0) + 1; });
palettes.forEach((p) => {
if (counts[p.name] > 1 && p.group)
p.name = p.group + ": " + p.name;
});
return palettes;
}
function getArtBit(fmt) {
return fmt.art ? (fmt.brev ? 0 : 7) : null;
}
/////////////////
// 0xabcd, #$abcd, 5'010101, 0b010101, etc
var pixel_re = /([0#]?)([x$%]|\d'h)([0-9a-f]+)(?:[;].*)?|(\d'b|0b)([01]+)/gim;
function convertToHexStatements(s) {
// 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;
});
}
function parseHexWords(s) {
var arr = [];
var m;
while (m = pixel_re.exec(s)) {
var n;
if (m[4])
n = parseInt(m[5], 2);
else if (m[2].startsWith('%'))
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;
}
function replaceHexWords(s, words, bpw) {
// convert 'hex ...' format to 0x prefixed values for regex matching
s = convertToHexStatements(s);
var result = "";
var m;
var li = 0;
var i = 0;
var nibbles = Math.ceil(bpw / 4);
while (m = pixel_re.exec(s)) {
result += s.slice(li, pixel_re.lastIndex - m[0].length);
li = pixel_re.lastIndex;
if (m[4])
result += m[4] + (0, util_1.tobin)(words[i++], parseInt(m[4]));
else if (m[2].startsWith('%'))
result += m[1] + m[2] + (0, util_1.tobin)(words[i++], bpw);
else if (m[2].endsWith('h'))
result += m[1] + m[2] + (0, util_1.hex)(words[i++], parseInt(m[2]));
else if (m[2].startsWith('x'))
result += m[1] + "x" + (0, util_1.hex)(words[i++], nibbles);
else if (m[2].startsWith('$'))
result += m[1] + "$" + (0, util_1.hex)(words[i++], nibbles);
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, arr) {
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;
}
// for VCS playfields
// ;;{w:20,h:10,flip:1,reindex:[4,5,6,7,15,14,13,12,11,10,9,8,16,17,18,19]};;
function reindexMask(x, inds) {
var i = inds[x % inds.length];
return [i >> 3, i & 7];
}
// Pac-Man strip layouts (must match src/machine/pacman.ts PacmanVideo)
const PAC_TILE_STRIPS = [
// [byteOffset, bx, by]
[0, 0, 4],
[8, 0, 0],
];
const PAC_SPRITE_STRIPS = [
[0, 8, 12], [8, 8, 0], [16, 8, 4], [24, 8, 8],
[32, 0, 12], [40, 0, 0], [48, 0, 4], [56, 0, 8],
];
function pacStripDecode(input, inOff, output, bx, by, imgWidth) {
for (var x = 0; x < 8; x++) {
var strip = input[inOff + x] || 0;
for (var y = 0; y < 4; y++) {
var i = (3 - y) * imgWidth + (7 - x);
var pen = ((strip >> y) & 1) | (((strip >> (y + 4)) & 1) << 1);
output[by * imgWidth + bx + i] = pen;
}
}
}
function pacStripEncode(output, outOff, pixels, bx, by, imgWidth) {
for (var x = 0; x < 8; x++) {
var strip = 0;
for (var y = 0; y < 4; y++) {
var i = (3 - y) * imgWidth + (7 - x);
var pen = (pixels[by * imgWidth + bx + i] || 0) & 3;
if (pen & 1)
strip |= (1 << y);
if (pen & 2)
strip |= (1 << (y + 4));
}
output[outOff + x] = strip;
}
}
function pacStripLayouts(fmt) {
if (fmt.w === 16 && fmt.h === 16)
return PAC_SPRITE_STRIPS;
return PAC_TILE_STRIPS;
}
function pacStripBytesPerImage(fmt) {
return pacStripLayouts(fmt).length * 8;
}
function convertPacStripWordsToImages(words, fmt) {
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || 1;
var wpimg = fmt.wpimg || pacStripBytesPerImage(fmt);
var layouts = pacStripLayouts(fmt);
var images = [];
for (var n = 0; n < count; n++) {
var pix = new Array(width * height).fill(0);
var base = n * wpimg;
for (var s = 0; s < layouts.length; s++) {
var [off, bx, by] = layouts[s];
pacStripDecode(words, base + off, pix, bx, by, width);
}
images.push(new Uint8Array(pix));
}
return images;
}
function convertPacStripImagesToWords(images, fmt) {
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || images.length;
var wpimg = fmt.wpimg || pacStripBytesPerImage(fmt);
var layouts = pacStripLayouts(fmt);
var words = new Array(wpimg * count).fill(0);
for (var n = 0; n < count; n++) {
var pix = images[n] || new Uint8Array(width * height);
var base = n * wpimg;
for (var s = 0; s < layouts.length; s++) {
var [off, bx, by] = layouts[s];
pacStripEncode(words, base + off, pix, bx, by, width);
}
}
return words;
}
function convertWordsToImages(words, fmt) {
if (fmt.pacstrip)
return convertPacStripWordsToImages(words, fmt);
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 wpimg = fmt.wpimg || wordsperline * height;
var rowstride = wordsperline;
if (fmt.il) {
wpimg = wordsperline;
rowstride = wordsperline * count;
}
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 = wpimg * n + yp * rowstride;
var shift = 0;
for (var x = 0; x < width; x++) {
var color = 0;
var ofs = remapBits(ofs0, fmt.remap);
if (fmt.reindex) {
[ofs, shift] = reindexMask(x, fmt.reindex);
ofs += ofs0;
}
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 && !fmt.reindex) {
ofs0 += 1;
shift = 0;
}
}
}
images.push(new Uint8Array(imgdata));
}
return images;
}
function computeRequiredWords(fmt) {
// mirrors the addressing performed by convertWordsToImages: the data must cover
// every word the converter can read, and must also span the full per-image block
// extent (wpimg * count), e.g. hardware sprite strides larger than the visible rows.
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 pofs = fmt.pofs || wordsperline * fmt.h * count;
var skip = fmt.skip || 0;
var wpimg = fmt.wpimg || (fmt.map === 'nesnt' ? 1024 : wordsperline * fmt.h);
var rowstride = wordsperline;
if (fmt.il) {
wpimg = wordsperline;
rowstride = wordsperline * count;
}
var maxReindexOfs = 0;
if (fmt.reindex) {
for (var x = 0; x < fmt.w; x++) {
maxReindexOfs = Math.max(maxReindexOfs, reindexMask(x, fmt.reindex)[0]);
}
}
var maxAddr = 0;
for (var n = 0; n < count; n++) {
for (var y = 0; y < fmt.h; y++) {
var yp = fmt.flip ? fmt.h - 1 - y : y;
var ofs0 = wpimg * n + yp * rowstride;
var steps = Math.ceil(fmt.w * bpp / bitsperword);
for (var s = 0; s < steps; s++) {
var ofs = fmt.reindex ? (ofs0 + maxReindexOfs) : remapBits(ofs0, fmt.remap);
maxAddr = Math.max(maxAddr, ofs + (nplanes - 1) * pofs + skip);
ofs0++;
}
}
}
return Math.max(wpimg * count, maxAddr + 1);
}
function validateAssetData(datastr, fmt) {
var words = parseHexWords(convertToHexStatements(datastr));
if (fmt.comp == 'rletag') {
words = Array.from((0, util_1.rle_unpack)(new Uint8Array(words)));
}
if (fmt.w > 0 && fmt.h > 0) {
var required = computeRequiredWords(fmt);
if (words.length != required) {
return `Expected ${required} value(s), found ${words.length}`;
}
}
else if (fmt.pal) {
if (words.length < 1) {
return `Palette requires at least 1 value, found ${words.length}`;
}
}
return null;
}
// same as validateAssetData, but for a raw binary file (e.g. referenced via #embed)
// where the byte count is already known and there's no source text to parse
function validateAssetByteLength(bytelen, fmt) {
if (fmt.comp == 'rletag') {
return null; // can't statically determine compressed length
}
if (fmt.w > 0 && fmt.h > 0) {
var required = computeRequiredWords(fmt);
if (bytelen != required) {
return `Expected ${required} byte(s), found ${bytelen}`;
}
}
else if (fmt.pal) {
if (bytelen < 1) {
return `Palette requires at least 1 byte, found ${bytelen}`;
}
}
return null;
}
function getLineNumber(data, offset) {
let line = 1;
for (let i = 0; i < offset && i < data.length; i++) {
if (data[i] === '\n')
line++;
}
return line;
}
// Scans source text for "/*{json}*/" or ";;{json};;" asset headers followed by a data
// block closed by the next ';' (C), ';;' (asm), or "end" (verilog). Pure/testable: no
// DOM or project dependencies -- platform-specific closing delimiter is supplied by caller.
function scanTextForAssetFragments(data, isVerilog) {
var re1 = /[/;][*;]([{].+[}])[*;][/;]/g;
var result = [];
var m;
while (m = re1.exec(data)) {
var start = m.index + m[0].length;
var end;
if (isVerilog) {
end = data.indexOf("end", start);
}
else if (m[0].startsWith(';;')) {
end = data.indexOf(';;', start);
if (end == data.indexOf(';;{', start)) {
// ignore start of next asset
end = -1;
}
}
else {
end = data.indexOf(';', start); // C
}
var startline = getLineNumber(data, m.index);
var header = m[0];
var endline = end >= 0 ? getLineNumber(data, end) : '???';
if (end < 0) {
var closingDelim = isVerilog ? '"end"' : m[0].startsWith(';;') ? '";;"' : '";"';
result.push({ header: header, startline: startline, endline: endline, error: `No closing ${closingDelim} found after asset header` });
}
else if (end <= start) {
result.push({ header: header, startline: startline, endline: endline, error: `Empty data block after asset header` });
}
else {
try {
var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON
var json = JSON.parse(jsontxt);
// C23 #embed "file.bin" -- data lives in a separate binary file, not inline
var embedMatch = /#embed\s+"(.+?)"/.exec(data.substring(start, end));
var embedFile = embedMatch ? embedMatch[1] : undefined;
result.push({ header: header, startline: startline, endline: endline, fmt: json, start: start, end: end, embedFile: embedFile });
}
catch (e) {
result.push({ header: header, startline: startline, endline: endline, error: `Invalid asset format: ${e.message}` });
}
}
}
return result;
}
// Resolves a #embed "file.bin" path against a project file-existence check: tries the
// plain name first, then a directory-prefixed candidate based on the including file's
// own path (e.g. "sub/main.c" + "data.bin" -> "sub/data.bin").
function resolveEmbedPath(fileid, embedName, fileExists) {
if (fileExists(embedName))
return embedName;
var slash = fileid.lastIndexOf('/');
if (slash >= 0) {
var candidate = fileid.substring(0, slash + 1) + embedName;
if (fileExists(candidate))
return candidate;
}
return null;
}
function convertImagesToWords(images, fmt) {
if (fmt.destfmt)
fmt = fmt.destfmt;
if (fmt.pacstrip)
return convertPacStripImagesToWords(images, fmt);
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 wpimg = fmt.wpimg || wordsperline * height;
var rowstride = wordsperline;
if (fmt.il) {
wpimg = wordsperline;
rowstride = wordsperline * count;
}
var totalwords = fmt.il ? rowstride * height : wpimg * count;
var words;
if (nplanes > 0 && fmt.sl) // TODO?
words = new Uint8Array(totalwords);
else if (bitsperword <= 8)
words = new Uint8Array(totalwords * nplanes);
else
words = new Uint32Array(totalwords * 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 * wpimg + yp * rowstride;
var shift = 0;
for (var x = 0; x < width; x++) {
var color = imgdata[i++];
var ofs = remapBits(ofs0, fmt.remap);
if (fmt.reindex) {
[ofs, shift] = reindexMask(x, fmt.reindex);
ofs += ofs0;
}
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 && !fmt.reindex) {
ofs0 += 1;
shift = 0;
}
}
}
}
return words;
}
// TODO
function convertPaletteBytes(arr, r0, r1, g0, g1, b0, b1) {
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;
}
/**
* Pac-Man / Namco color PROM byte → RGBA.
* Must match src/machine/pacman.ts PacmanVideo.rebuild() (MAME resistor weights).
* Encoding: RRR (bits 0-2), GGG (3-5), BB (6-7).
*/
function decodePacmanColorPromByte(d) {
var r = ((d >> 0) & 1) * 0x21 + ((d >> 1) & 1) * 0x47 + ((d >> 2) & 1) * 0x97;
var g = ((d >> 3) & 1) * 0x21 + ((d >> 4) & 1) * 0x47 + ((d >> 5) & 1) * 0x97;
var b = ((d >> 6) & 1) * 0x51 + ((d >> 7) & 1) * 0xae;
return 0xff000000 | (b << 16) | (g << 8) | r;
}
function getPaletteLength(palfmt) {
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 if (pal === 'pacman') {
return 256; // every PROM encoding byte → a color
}
else {
var paltable = PREDEF_PALETTES[pal];
if (paltable) {
return paltable.length;
}
else {
throw new Error("No palette named " + pal);
}
}
}
function convertPaletteFormat(palbytes, palfmt) {
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 if (pal === 'pacman') {
newpalette = [];
for (var i = 0; i < palbytes.length; i++)
newpalette.push(decodePacmanColorPromByte(palbytes[i]));
}
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;
}
function getDirectColorChannels(palfmt) {
var pal = palfmt.pal;
if (typeof pal !== 'number' || !isFinite(pal))
return null;
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);
if (rr + gg + bb <= 0)
return null;
if (pal >= 0)
return { r: rr, g: gg, b: bb, rshift: 0, gshift: rr, bshift: rr + gg };
// negative pal: convertPaletteBytes(arr, rr+gg, bb, rr, gg, 0, rr)
return { r: bb, g: gg, b: rr, rshift: rr + gg, gshift: rr, bshift: 0 };
}
// Pack per-channel values into a direct-color palette word (inverse of
// decodeDirectColorWord / convertPaletteFormat).
function encodeDirectColorWord(palfmt, r, g, b) {
var c = getDirectColorChannels(palfmt);
if (!c)
return 0;
return ((r & ((1 << c.r) - 1)) << c.rshift)
| ((g & ((1 << c.g) - 1)) << c.gshift)
| ((b & ((1 << c.b) - 1)) << c.bshift);
}
function decodeDirectColorWord(palfmt, word) {
var c = getDirectColorChannels(palfmt);
if (!c)
return null;
return {
r: (word >> c.rshift) & ((1 << c.r) - 1),
g: (word >> c.gshift) & ((1 << c.g) - 1),
b: (word >> c.bshift) & ((1 << c.b) - 1),
};
}
// TODO: illegal colors?
const PREDEF_PALETTES = {
// LCD polarity: index 0 = lightest (pixel off), 3 = darkest (pixel on)
'gb': [
0xd0d884, 0x87a84c, 0x526b34, 0x2d3122,
],
'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
],
'astrocade': [0, 2368548, 4737096, 7171437, 9539985, 11974326, 14342874, 16777215, 12255269, 14680137, 16716142, 16725394, 16734903, 16744155, 16753663, 16762879, 11534409, 13959277, 16318866, 16721334, 16730842, 16740095, 16749311, 16758783, 10420330, 12779662, 15138995, 16718039, 16727291, 16736767, 16745983, 16755199, 8847495, 11206827, 13631696, 15994612, 16724735, 16733951, 16743423, 16752639, 6946975, 9306307, 11731175, 14092287, 16461055, 16732415, 16741631, 16751103, 4784304, 7143637, 9568505, 11929087, 14297599, 16731647, 16741119, 16750335, 2425019, 4784352, 7209215, 9570047, 12004095, 14372863, 16741375, 16750847, 191, 2359523, 4718847, 7146495, 9515263, 11949311, 14318079, 16752127, 187, 224, 2294015, 4658431, 7092735, 9461247, 11895551, 14264063, 176, 213, 249, 2367999, 4736511, 7105279, 9539327, 11908095, 159, 195, 3303, 209151, 2577919, 4946431, 7380735, 9749247, 135, 171, 7888, 17140, 681983, 3050495, 5484543, 7853311, 106, 3470, 12723, 22231, 31483, 1548031, 3916799, 6285311, 73, 8557, 17810, 27318, 36570, 373759, 2742271, 5176575, 4389, 13641, 23150, 32402, 41911, 51163, 2026495, 4456447, 9472, 18724, 27976, 37485, 46737, 56246, 1834970, 4194303, 14080, 23296, 32803, 42055, 51564, 60816, 2031541, 4456409, 18176, 27648, 36864, 46116, 55624, 392556, 2752401, 5177269, 21760, 30976, 40192, 49667, 58919, 1572683, 3932016, 6291348, 24320, 33536, 43008, 52224, 716810, 3079982, 5504851, 7864183, 25856, 35328, 44544, 250368, 2619136, 4980503, 7405371, 9764703, 26624, 35840, 45312, 2413824, 4782336, 7143173, 9568041, 11927374, 26112, 35584, 2338560, 4707328, 7141376, 9502464, 11927326, 14286659, 24832, 2393344, 4762112, 7196160, 9564928, 11992832, 14352155, 16711487, 2447360, 4815872, 7250176, 9618688, 12052992, 14417664, 16776990, 16777027, 4803328, 7172096, 9606144, 11974912, 14343424, 16776965, 16777001, 16777038, 6962176, 9330688, 11764992, 14133504, 16502272, 16773655, 16777019, 16777055, 8858112, 11226880, 13660928, 16029440, 16759818, 16769070, 16777043, 16777079, 10426112, 12794624, 15163392, 16745475, 16754727, 16764235, 16773488, 16777108, 11534848, 13969152, 16337664, 16740388, 16749640, 16759148, 16768401, 16777141, 12255232, 14684928, 16725795, 16735047, 16744556, 16753808, 16763317, 16772569],
'c64': [0x000000, 0xffffff, 0x2b3768, 0xb2a470, 0x863d6f, 0x438d58, 0x792835, 0x6fc7b8, 0x254f6f, 0x003943, 0x59679a, 0x444444, 0x6c6c6c, 0x84d29a, 0xb55e6c, 0x959595],
};
// SMS and Game Gear share the same mode-4 CRAM layout: 32 colors where the
// tilemap's bit 11 selects palette 0 (background, colors 0-15) or palette 1
// (sprites, colors 16-31). Game Gear packs 12-bit color, SMS 6-bit, but the
// index layout is identical, so one layout serves both.
var SMS_GG_LAYOUT = [
['Background', 0x00, 16],
['Sprite', 0x10, 16],
];
exports.PREDEF_LAYOUTS = {
'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]
],
'astrocade': [
['Left', 0x00, -4],
['Right', 0x04, -4]
],
// Pac-Man: each palette PROM entry is 4 pens (indices into the 32-color PROM).
// Arrays tagged layout:"pacman" should store those pens as already-resolved color-PROM bytes.
'pacman': [
['Pal 01', 0x00, 4],
['Pal 03', 0x04, 4],
['Pal 05', 0x08, 4],
['Pal 07', 0x0c, 4],
['Pal 09', 0x10, 4],
['Pal 10', 0x14, 4],
['Pal 11', 0x18, 4],
['Pal 14', 0x1c, 4],
['Pal 19', 0x20, 4],
],
'sms': SMS_GG_LAYOUT,
'gg': SMS_GG_LAYOUT,
};
// Slice a converted RGBA palette into the named sub-palettes described by a
// layout, keeping only slices whose color count matches `matchlen` (the pen
// count of the image being colored, i.e. 2^bpp). Negative lengths produce a
// reversed slice; a slice one color shorter than matchlen gets the shared
// color 0 prepended (the NES backdrop convention).
function computePaletteSlices(palette, layout, matchlen, node, group) {
var result = [];
if (!layout)
return result;
layout.forEach(([name, start, len]) => {
if (start >= palette.length)
return;
if (len == matchlen) {
result.push({ node, name, group, palette: palette.slice(start, start + len) });
}
else if (-len == matchlen) { // reverse order
var reversed = palette.slice(start, start - len);
reversed.reverse();
result.push({ node, name, group, palette: reversed });
}
else if (len + 1 == matchlen) {
var shared = new Uint32Array(matchlen);
shared[0] = palette[0];
shared.set(palette.slice(start, start + len), 1);
result.push({ node, name, group, palette: shared });
}
});
return result;
}
/////
function equalArrays(a, b) {
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, b) {
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;
}
class PixNode {
refreshLeft() {
var p = this;
while (p) {
p.updateLeft();
p = p.left;
}
}
refreshRight() {
var p = this;
while (p) {
p.updateRight();
p = p.right;
}
}
addRight(node) {
this.right = node;
node.left = this;
return node;
}
addLeft(node) {
this.left = node;
node.right = this;
return node;
}
}
exports.PixNode = PixNode;
class CodeProjectDataNode extends PixNode {
}
class FileDataNode extends CodeProjectDataNode {
constructor(project, fileid) {
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);
}
return true;
}
updateRight() {
if (this.project) {
this.words = this.project.project.getFile(this.fileid);
}
return true;
}
}
exports.FileDataNode = FileDataNode;
class TextDataNode extends CodeProjectDataNode {
constructor(project, fileid, label, start, end, bpw) {
super();
this.project = project;
this.fileid = fileid;
this.label = label;
this.bpw = bpw || 8;
this.rangeId = `asset_${TextDataNode.nextRangeId++}`;
this.project.setAssetRange(this.fileid, this.rangeId, start, end);
}
updateLeft() {
if (this.right.words.length != this.words.length)
throw Error("Cannot put " + this.right.words.length + " image bytes into array of " + this.words.length + " bytes");
this.words = this.right.words;
var datastr = this.project.getAssetText(this.fileid, this.rangeId);
if (datastr == null)
throw Error("Could not find source code range for asset " + this.fileid);
datastr = replaceHexWords(datastr, this.words, this.bpw);
// CM6 state field automatically remaps all tracked ranges.
this.project.replaceAssetText(this.fileid, this.rangeId, datastr);
return true;
}
updateRight() {
var datastr = this.project.getAssetText(this.fileid, this.rangeId);
if (datastr == null)
throw Error("Could not find source code range for asset " + this.fileid);
datastr = convertToHexStatements(datastr);
var words = parseHexWords(datastr);
this.words = words;
return true;
}
}
exports.TextDataNode = TextDataNode;
TextDataNode.nextRangeId = 0;
class Compressor extends PixNode {
updateLeft() {
// TODO: can't modify length of rle bytes
return false;
}
updateRight() {
this.words = (0, util_1.rle_unpack)(new Uint8Array(this.left.words));
return true;
}
}
exports.Compressor = Compressor;
class Mapper extends PixNode {
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;
}
}
exports.Mapper = Mapper;
class RGBAPalette {
constructor(palcols) {
this.palcols = palcols;
}
indexOf(rgba) {
return this.palcols.indexOf(rgba);
}
}
class Palettizer extends PixNode {
// TODO: control to select palette for bitmaps
constructor(context, fmt) {
super();
this.palindex = 0;
this.palselected = false; // true once the user chooses from the dropdown
this.context = context;
// Pac-Man strip bitmaps are always 2bpp (4 pens), even if bpp is omitted.
if (fmt.pacstrip)
this.ncolors = 4;
else
this.ncolors = 1 << ((fmt.bpp || 1) * (fmt.np || 1));
this.palname = fmt.palname;
}
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) => {
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) => {
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) {
// Pick the palette named by fmt.palname until the user chooses one.
if (this.palname && !this.palselected) {
var pi = this.paloptions.findIndex((p) => p.name === this.palname);
if (pi >= 0)
this.palindex = pi;
}
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;
}
}
exports.Palettizer = Palettizer;
function dedupPalette(cols) {
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;
}
class PaletteFormatToRGB extends PixNode {
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;
var cols = convertPaletteFormat(this.words, this.palfmt);
// Keep exact PROM colors for Pac-Man (and any layout slices). Dedup is only
// for flat palette grids where identical swatches need unique edit identities.
this.palette = (this.palfmt.layout || this.palfmt.pal === 'pacman')
? new Uint32Array(cols) : dedupPalette(cols);
this.layout = exports.PREDEF_LAYOUTS[this.palfmt.layout];
this.rgbimgs = [];
this.palette.forEach((rgba) => {
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);
}
}
exports.PaletteFormatToRGB = PaletteFormatToRGB;
class PaletteEditorView extends PixNode {
constructor(updateCells) {
super();
this.updateCells = updateCells;
}
updateLeft() {
return true;
}
updateRight() {
this.updateCells();
return true;
}
}
exports.PaletteEditorView = PaletteEditorView;
class Compositor extends PixNode {
constructor(context) {
super();
this.tileindex = 0;
this.context = context;
}
updateRefs() {
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);
}
}
exports.Compositor = Compositor;
class MetaspriteCompositor extends Compositor {
constructor(context, 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;
}
}
exports.MetaspriteCompositor = MetaspriteCompositor;
class NESNametableConverter extends Compositor {
constructor(context) {
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];
if (typeof name === 'undefined')
throw Error("No name for address " + this.baseofs + " + " + a);
var t = this.tilemap[name];
if (!t)
throw Error("No tilemap found for tile index " + 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;
}
}
exports.NESNametableConverter = NESNametableConverter;
///// UI CONTROLS
class ImageChooser {
constructor() {
this.gapX = null;
}
recreate(parentdiv, onclick) {
var agrid = $('<div class="asset_grid"/>'); // grid (or 1) of preview images
parentdiv.empty().append(agrid);
this.viewers = [];
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.gapX = this.gapX;
viewer.recreate();
viewer.canvas.style.width = (viewer.displayWidth * cscale) + 'px'; // TODO
viewer.canvas.title = '$' + (0, util_1.hex)(i);
viewer.updateImage(imdata);
$(viewer.canvas).addClass('asset_cell');
$(viewer.canvas).click((e) => {
onclick(i, viewer);
});
this.viewers.push(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;
}
});
}
updateImages(rgbimgs) {
this.rgbimgs = rgbimgs;
for (var i = 0; i < this.viewers.length; i++) {
this.viewers[i].updateImage(rgbimgs[i]);
}
}
}
exports.ImageChooser = ImageChooser;
// A color picker for a single palette entry.
//
// Direct-color formats (pal:444, pal:332, ...) encode RGB in the word itself;
// a 444 palette has 4096 possible colors so showing a swatch for each one is
// unusable. Those get an RGB color input plus per-channel sliders. Indexed
// palettes get a scrollable swatch grid (their tables are small).
class PaletteColorPicker {
constructor(palfmt, rgbimgs, onpick) {
this.viewers = [];
this.sliders = [];
this.currentWord = 0;
this.palfmt = palfmt;
this.rgbimgs = rgbimgs;
this.channels = getDirectColorChannels(palfmt);
this.onpick = onpick;
}
recreate(parentdiv, currentWord) {
parentdiv.empty();
this.viewers = [];
this.sliders = [];
this.previewEl = null;
this.colorInput = null;
this.hexEl = null;
if (this.channels)
this.createSliders(parentdiv);
else
this.createGrid(parentdiv);
this.setCurrent(currentWord);
}
paletteColor(word) {
var cols = convertPaletteFormat([word], this.palfmt);
return (cols && cols[0]) || 0xff000000;
}
updateColorWidgets(word) {
var rgb = this.paletteColor(word);
var hexcol = '#' + (0, util_1.hex)((0, util_1.rgb2bgr)(rgb & 0xffffff), 6);
if (this.previewEl)
this.previewEl.css('background-color', hexcol);
if (this.colorInput && this.colorInput.value !== hexcol)
this.colorInput.value = hexcol;
if (this.hexEl)
this.hexEl.text(hexcol + ' $' + (0, util_1.hex)(word, 4));
}
setCurrent(word) {
this.currentWord = word;
if (this.channels) {
var c = decodeDirectColorWord(this.palfmt, word);
if (c) {
for (var s of this.sliders) {
var v = s.channel === 'r' ? c.r : s.channel === 'g' ? c.g : c.b;
s.input.value = String(v);
s.valueLabel.text(String(v));
}
}
this.updateColorWidgets(word);
}
else {
this.viewers.forEach((v, i) => $(v.canvas).toggleClass('selected', i === word));
}
}
channelBits(name) {
var c = this.channels;
if (!c)
return 0;
return name === 'r' ? c.r : name === 'g' ? c.g : c.b;
}
pickFromSliders() {
var vals = { r: 0, g: 0, b: 0 };
for (var s of this.sliders)
vals[s.channel] = Number(s.input.value);
var word = encodeDirectColorWord(this.palfmt, vals['r'], vals['g'], vals['b']);
this.setCurrent(word);
this.onpick(word);
}
createSliders(parentdiv) {
var c = this.channels;
var box = newDiv(parentdiv, 'asset_colorpicker');
this.previewEl = newDiv(box, 'asset_colorpicker_preview');
// native <input type=color> only addresses 8 bits/channel
if (c.r <= 8 && c.g <= 8 && c.b <= 8) {
this.colorInput = document.createElement('input');
this.colorInput.type = 'color';
this.colorInput.className = 'asset_colorpicker_colorinput';
box.append(this.colorInput);
}
this.hexEl = newDiv(box, 'asset_colorpicker_hex');
var controls = newDiv(box, 'asset_colorpicker_controls');
for (var name of ['r', 'g', 'b']) {
var bits = this.channelBits(name);
if (bits <= 0)
continue;
var row = newDiv(controls, 'asset_colorpicker_row');
$('<label/>').text(name.toUpperCase()).appendTo(row);
var input = document.createElement('input');
input.type = 'range';
input.min = '0';
input.max = String((1 << bits) - 1);
input.step = '1';
input.className = 'asset_colorpicker_slider';
row.append(input);
var valueLabel = $('<span class="asset_colorpicker_value"/>').appendTo(row);
input.value = '0';
this.sliders.push({ input, valueLabel, channel: name });
$(input).on('input', () => this.pickFromSliders());
}
if (this.colorInput)
$(this.colorInput).on('input', () => {
// native color input is 8 bits/channel; quantize to the palette's depth
var v = this.colorInput.value;
var r = parseInt(v.substring(1, 3), 16);
var g = parseInt(v.substring(3, 5), 16);
var b = parseInt(v.substring(5, 7), 16);
var word = encodeDirectColorWord(this.palfmt, r >> (8 - c.r), g >> (8 - c.g), b >> (8 - c.b));
this.setCurrent(word);
this.onpick(word);
});
}
createGrid(parentdiv) {
var scroll = newDiv(parentdiv, 'asset_colorpicker_grid');
var agrid = newDiv(scroll, 'asset_grid');
var imgsperline = 16;
var span = null;
this.rgbimgs.forEach((imdata, i) => {
var viewer = new Viewer();
viewer.width = 1;
viewer.height = 1;
viewer.recreate();
viewer.canvas.style.width = '16px';
viewer.canvas.style.height = '16px';
viewer.updateImage(imdata);
$(viewer.canvas).addClass('asset_cell');
$(viewer.canvas).attr('title', '$' + (0, util_1.hex)(i) + ' #' + (0, util_1.hex)((0, util_1.rgb2bgr)(imdata[0] & 0xffffff), 6));
$(viewer.canvas).click(() => {
this.setCurrent(i);
this.onpick(i);
});
this.viewers.push(viewer);
if (!span) {
span = $('<span/>');
agrid.append(span);
}
span.append(viewer.canvas);
if ((i % imgsperline) === imgsperline - 1) {
agrid.append($('<br/>'));
span = null;
}
});
}
}
exports.PaletteColorPicker = PaletteColorPicker;
function newDiv(parent, cls) {
var div = $(document.createElement("div"));
if (parent)
div.appendTo(parent);
if (cls)
div.addClass(cls);
return div;
}
class CharmapEditor extends PixNode {
constructor(context, parentdiv, fmt) {
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;
// if chooser already exists with same number of images, update in place
if (this.chooser && this.chooser.viewers && this.chooser.viewers.length == this.rgbimgs.length) {
this.updateArtInfo();
this.chooser.updateImages(this.rgbimgs);
// keep rgbimgs pointing to viewer buffers so edits propagate back via refreshLeft
for (var i = 0; i < this.chooser.viewers.length; i++) {
this.rgbimgs[i] = this.chooser.viewers[i].rgbdata;
}
// the palette (e.g. changed via the selector) also drives the editor's
// selectable color buttons, so update them to match
if (this.editor)
this.editor.setPalette(this.left.palette);
return true;
}
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;
var artBit = getArtBit(this.fmt);
if (artBit != null) {
chooser.gapX = this.fmt.brev ? artBit + 1 : artBit;
}
chooser.recreate(agrid, (index, viewer) => {
// TODO: variable scale?
const aspect = this.fmt.aspect || 1.0;
const yscale = Math.min(MAX_SCALE, MAX_SIZE_X / (viewer.displayWidth * aspect), MAX_SIZE_Y / this.fmt.h);
const xscale = yscale * aspect;
this.createEditor(aeditor, viewer, xscale, yscale);
this.context.setCurrentEditor(aeditor, $(viewer.canvas), this);
this.rgbimgs[index] = viewer.rgbdata;
});
this.updateArtInfo();
if (this.fmt.art) {
for (var i = 0; i < this.chooser.viewers.length; i++) {
this.chooser.viewers[i].updateImage();
}
}
// 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.palindex)
sel.attr('selected', 'selected');
});
palselect.appendTo(agrid).change((e) => {
var index = $(e.target).val();
palizer.palindex = index;
palizer.palselected = true;
palizer.refreshRight();
});
}
return true;
}
updateArtInfo() {
if (!this.fmt.art || !this.chooser || !this.chooser.viewers)
return;
var bitsperword = this.fmt.bpw || 8;
var artBit = getArtBit(this.fmt);
var indexedImages = this.left.images;
if (!indexedImages)
return;
for (var i = 0; i < this.chooser.viewers.length; i++) {
var viewer = this.chooser.viewers[i];
viewer.artInfo = {
artBit: artBit,
bitsperword: bitsperword,
indexedImage: indexedImages[i]
};
}
}
createEditor(aeditor, viewer, xscale, yscale) {
var im = new PixEditor();
im.createWith(viewer);
im.updateImage();
var w = im.displayWidth * xscale;
var h = viewer.height * yscale;
im.canvas.style.width = w + 'px'; // TODO
im.canvas.style.height = h + 'px'; // TODO
im.makeEditable(this, aeditor, this.left.palette);
this.editor = im;
return im;
}
}
exports.CharmapEditor = CharmapEditor;
class MapEditor extends PixNode {
constructor(context, parentdiv, fmt) {
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 viewer = new Viewer();
viewer.width = this.fmt.w;
viewer.height = this.fmt.h;
viewer.recreate();
viewer.updateImage(this.rgbimgs[0]);
agrid.append(viewer.canvas);
return true;
}
}
exports.MapEditor = MapEditor;
class Viewer {
constructor() {
this.gapX = null;
this.artInfo = null;
}
get displayWidth() {
return this.width + (this.gapX != null ? 1 : 0);
}
recreate() {
this.canvas = this.newCanvas();
this.imagedata = this.ctx.createImageData(this.width, this.height);
this.rgbdata = new Uint32Array(this.imagedata.data.buffer);
if (this.gapX != null) {
this.displayImageData = this.ctx.createImageData(this.displayWidth, this.height);
this.displayRgbData = new Uint32Array(this.displayImageData.data.buffer);
}
this.peerviewers = [this];
}
createWith(pv) {
this.width = pv.width;
this.height = pv.height;
this.gapX = pv.gapX;
this.imagedata = pv.imagedata;
this.rgbdata = pv.rgbdata;
this.canvas = this.newCanvas();
if (this.gapX != null) {
this.displayImageData = this.ctx.createImageData(this.displayWidth, this.height);
this.displayRgbData = new Uint32Array(this.displayImageData.data.buffer);
}
pv.peerviewers.push(this);
this.peerviewers = pv.peerviewers;
this.artInfo = pv.artInfo;
}
newCanvas() {
var c = document.createElement('canvas');
c.width = this.displayWidth;
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) {
if (imdata) {
this.rgbdata.set(imdata);
}
for (let v of this.peerviewers) {
if (v.gapX != null && v.displayRgbData) {
var gx = v.gapX;
var dw = v.displayWidth;
for (var y = 0; y < this.height; y++) {
var srcOfs = y * this.width;
var dstOfs = y * dw;
for (var x = 0; x < gx; x++) {
v.displayRgbData[dstOfs + x] = this.rgbdata[srcOfs + x];
}
v.displayRgbData[dstOfs + gx] = 0x00000000; // Transparent gap column.
for (var x = gx; x < this.width; x++) {
v.displayRgbData[dstOfs + x + 1] = this.rgbdata[srcOfs + x];
}
}
v.ctx.putImageData(v.displayImageData, 0, 0);
}
else {
v.ctx.putImageData(this.imagedata, 0, 0);
}
}
}
}
exports.Viewer = Viewer;
var pixelClipboard = null;
function getPixelClipboard() {
return pixelClipboard;
}
function setPixelClipboard(clip) {
pixelClipboard = clip;
}
// Paste clipboard pixels over the top-left of the destination image, clipped to
// whichever is smaller. Colors are matched by palette index, not by RGB value,
// so a tile copied under one palette keeps its shape under another.
function pasteClipboardPixels(clip, dstpixels, dstwidth, dstheight, dstpalette) {
var remap = new Map();
for (var i = 0; i < clip.palette.length; i++) {
if (!remap.has(clip.palette[i]))
remap.set(clip.palette[i], dstpalette[i & (dstpalette.length - 1)]);
}
var result = dstpixels.slice();
var w = Math.min(clip.width, dstwidth);
var h = Math.min(clip.height, dstheight);
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var rgba = clip.pixels[y * clip.width + x];
var mapped = remap.get(rgba);
result[y * dstwidth + x] = mapped !== undefined ? mapped : rgba;
}
}
return result;
}
class PixEditor extends Viewer {
constructor() {
super(...arguments);
this.curpalcol = -1;
this.offscreen = new Map();
}
getPositionFromEvent(e) {
var dw = this.displayWidth;
var x = Math.floor(e.offsetX * dw / $(this.canvas).width());
var y = Math.floor(e.offsetY * this.height / $(this.canvas).height());
if (this.gapX != null && x > this.gapX)
x--;
return { x: x, y: y };
}
setPaletteColor(col) {
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, aeditor, palette) {
this.left = leftnode;
this.palette = palette;
var dragcol;
var dragging = false;
var pxls = $(this.canvas);
var artDragIdx = -1;
pxls.mousedown((e) => {
var pos = this.getPositionFromEvent(e);
if (this.isArtPixel(pos.x)) {
artDragIdx = this.toggleArtPixel(pos.x, pos.y);
dragging = true;
$(document).mouseup((e) => {
$(document).off('mouseup');
dragging = false;
artDragIdx = -1;
this.commit();
});
return;
}
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) => {
$(document).off('mouseup');
var pos = this.getPositionFromEvent(e);
this.setPixel(pos.x, pos.y, dragcol);
dragging = false;
this.commit();
});
})
.mousemove((e) => {
var pos = this.getPositionFromEvent(e);
if (!dragging)
return;
if (artDragIdx >= 0 && this.isArtPixel(pos.x)) {
this.setArtPixel(pos.x, pos.y, artDragIdx);
}
else if (artDragIdx < 0 && !this.isArtPixel(pos.x)) {
this.setPixel(pos.x, pos.y, dragcol);
}
});
aeditor.empty();
this.aeditor = aeditor;
this.createToolbarButtons(aeditor[0]);
aeditor.append(this.canvas);
aeditor.append(this.createPaletteButtons());
this.setPaletteColor(1);
}
// Update the palette used for the selectable color buttons (e.g. after the
// palette selector dropdown changes). Rebuilds the buttons and keeps the
// current selection valid for the new palette size.
setPalette(palette) {
if (this.palette === palette)
return;
this.palette = palette;
this.curpalcol = -1;
if (this.aeditor) {
this.palbtnspan.remove();
this.aeditor.append(this.createPaletteButtons());
}
this.setPaletteColor(1);
}
isArtPixel(x) {
if (!this.artInfo)
return false;
return (x % this.artInfo.bitsperword) == this.artInfo.artBit;
}
toggleArtPixel(x, y) {
var info = this.artInfo;
if (!info)
return -1;
var ofs = y * this.width + x;
var newIdx = info.indexedImage[ofs] ? 0 : 1;
this.setArtPixel(x, y, newIdx);
return newIdx;
}
setArtPixel(x, y, idx) {
var info = this.artInfo;
if (!info)
return;
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
return;
var ofs = y * this.width + x;
if (info.indexedImage[ofs] == idx)
return;
info.indexedImage[ofs] = idx;
this.rgbdata[ofs] = this.palette[idx];
this.updateImage();
}
getPixel(x, y) {
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, y, rgba) {
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 = this.palbtnspan = 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 = "#" + (0, util_1.hex)((0, util_1.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;
}
createToolbarButtons(parent) {
var toolbar = new toolbar_1.Toolbar(parent, parent); // scope key bindings to the pixel editor
toolbar.add('ctrl+shift+h', 'Flip X', 'glyphicon-resize-horizontal', this.flipX.bind(this));
toolbar.add('ctrl+shift+v', 'Flip Y', 'glyphicon-resize-vertical', this.flipY.bind(this));
toolbar.add('ctrl+shift+9', 'Rotate', 'glyphicon-repeat', this.rotate90.bind(this));
toolbar.add('ctrl+shift+left', 'Move Left', 'glyphicon-arrow-left', this.translate.bind(this, 1, 0));
toolbar.add('ctrl+shift+right', 'Move Right', 'glyphicon-arrow-right', this.translate.bind(this, -1, 0));
toolbar.add('ctrl+shift+up', 'Move Up', 'glyphicon-arrow-up', this.translate.bind(this, 0, 1));
toolbar.add('ctrl+shift+down', 'Move Down', 'glyphicon-arrow-down', this.translate.bind(this, 0, -1));
toolbar.newGroup();
toolbar.add(null, 'Copy Image', 'glyphicon-copy', this.copyImage.bind(this));
toolbar.add(null, 'Paste Image', 'glyphicon-paste', this.pasteImage.bind(this));
// TODO: destroy toolbar?
}
// returns the image with art-bit columns removed, like remapPixels() operates on
getStrippedPixels() {
if (!this.artInfo) {
return { pixels: this.rgbdata.slice(), width: this.width };
}
var bpw = this.artInfo.bitsperword;
var strippedWidth = this.width - Math.floor(this.width / bpw);
var stripped = new Uint32Array(strippedWidth * this.height);
this.copyNonArtPixels(stripped, strippedWidth, this.rgbdata, this.width);
return { pixels: stripped, width: strippedWidth };
}
copyImage() {
var { pixels, width } = this.getStrippedPixels();
setPixelClipboard({
width: width,
height: this.height,
pixels: pixels,
palette: this.palette.slice()
});
}
pasteImage() {
var clip = getPixelClipboard();
if (!clip)
return;
var { pixels, width } = this.getStrippedPixels();
var pasted = pasteClipboardPixels(clip, pixels, width, this.height, this.palette);
this.remapPixels((x, y) => pasted[y * width + x]);
}
commit() {
this.updateImage();
try {
this.left.refreshLeft();
}
catch (e) {
console.log(e);
alert(`Could not update source code. ${e}`);
}
}
copyNonArtPixels(dst, dstWidth, src, srcWidth) {
for (var y = 0; y < this.height; y++) {
var si = 0, di = 0;
for (var x = 0; x < this.width; x++) {
var isArt = this.isArtPixel(x);
if (!isArt)
dst[y * dstWidth + di] = src[y * srcWidth + si];
if (!isArt || srcWidth === this.width)
si++;
if (!isArt || dstWidth === this.width)
di++;
}
}
}
remapPixels(mapfn) {
// Strip art bit columns so transforms ignore them.
var savedRgb = this.rgbdata;
var savedWidth = this.width;
if (this.artInfo) {
var bpw = this.artInfo.bitsperword;
var strippedWidth = this.width - Math.floor(this.width / bpw);
var stripped = new Uint32Array(strippedWidth * this.height);
this.copyNonArtPixels(stripped, strippedWidth, this.rgbdata, this.width);
this.rgbdata = stripped;
this.width = strippedWidth;
}
// Apply the transform.
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);
}
}
// Restore art bit columns from the original data.
this.rgbdata = savedRgb;
this.width = savedWidth;
if (this.artInfo) {
this.copyNonArtPixels(this.rgbdata, this.width, pixels, strippedWidth);
}
else {
this.rgbdata.set(pixels);
}
this.commit();
}
rotate(deg) {
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(xx2, yy2);
});
}
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, dy) {
this.remapPixels((x, y) => {
return this.getPixel(x + dx, y + dy);
});
}
}
// TODO: not yet used
class TwoWayPixelConverter {
constructor(width, height, bpp, colpp) {
this.w = width;
this.h = height;
this.words = new Uint8Array(width * height);
this.bitoffsets = new Uint32Array(width * height);
this.coloffsets = new Uint32Array(width * height);
}
setPixel(x, y, col, bitofs, colofs) {
var ofs = x + y * this.w;
this.words[ofs] = col;
this.bitoffsets[ofs] = bitofs;
this.coloffsets[ofs] = colofs;
}
}
class TwoWayMapper extends PixNode {
constructor(pc) {
super();
this.pc = pc;
}
updateLeft() {
//if (equalNestedArrays(this.images, this.right.images)) return false;
this.images = this.right.images;
this.words = this.pc.imageToWords(this.images[0]);
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 = this.pc.wordsToImage(this.words);
return true;
}
}
exports.TwoWayMapper = TwoWayMapper;
//# sourceMappingURL=pixeleditor.js.map