nftr-editor/js/nftr.js
2023-03-20 08:19:47 -05:00

1333 lines
41 KiB
JavaScript
Raw Permalink Blame History

let encoding, tileWidth, tileHeight, tileSize, tileBitDepth, fontTiles, fontWidths, bytesPerWidth, fontMap, questionMark = 0, questionMarkChar = "";
let maxChar = 0;
let palette = [[0xFF, 0xFF, 0xFF, 0x00], [0x92, 0x92, 0x92, 0xFF], [0x43, 0x43, 0x43, 0xFF], [0x00, 0x00, 0x00, 0xFF]];
let paletteHTML = ["", "#929292", "#434343", "#000000"];
let data, fontU8, fileName;
let brushColor = 0, realColor = 0, extraKerning = 0, scale = 1;
var onkeydown, onkeyup;
function loadFont(file) {
if(!file) {
alert("No file selected!");
if(document.getElementById("editBox").classList.contains("show")) {
new bootstrap.Collapse(document.getElementById("editBox"), {toggle: false});
new bootstrap.Collapse(document.getElementById("saveButton"), {toggle: false});
}
window.onbeforeunload = function() { return; };
return false;
}
fileName = file.name;
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() { reloadFont(this.result); };
}
function reloadFont(buffer) {
fontU8 = new Uint8Array(buffer);
data = new DataView(fontU8.buffer);
let offset = 0x14;
// Get encoding
encoding = data.getUint8(0x1F);
// Skip font info
offset += data.getUint8(0x14);
// Load glyph info
let chunkSize = data.getUint32(offset, true);
offset += 4;
tileWidth = data.getUint8(offset++);
tileHeight = data.getUint8(offset++);
tileSize = data.getUint16(offset, true);
offset += 2;
offset += 2; // skip underline and max proportional width
tileBitDepth = data.getUint8(offset++);
// Load character glyphs
let tileAmount = ((chunkSize - 0x10) / tileSize);
offset++;
fontTiles = [];
for(let i = 0; i < tileAmount; i++) {
fontTiles.push(new Uint8Array(buffer.slice(offset + (i * tileSize), offset + ((i + 1) * tileSize))));
}
// Fix top row
// TODO: Maybe don't do this? Look into what these mean
// for(let i = 0; i < tileAmount; i++) {
// fontTiles[i * tileSize] = 0;
// fontTiles[i * tileSize + 1] = 0;
// fontTiles[i * tileSize + 2] = 0;
// }
// Load character widths
offset = data.getUint32(0x24, true) - 4;
chunkSize = data.getUint32(offset, true);
offset += 4 + 2;
let charCount = data.getUint16(offset, true) + 1;
maxChar = charCount;
offset += 2 + 4;
fontWidths = [];
// Some fonts don't have the total size
bytesPerWidth = Math.min(3, Math.floor((chunkSize - 0x10) / tileAmount));
for(let i = 0; i < tileAmount; i++) {
fontWidths.push(new Uint8Array(buffer.slice(offset + (i * bytesPerWidth), offset + ((i + 1) * bytesPerWidth))));
}
// Load character maps
fontMap = new Uint16Array(charCount);
let locPAMC = data.getUint32(0x28, true);
while(locPAMC < fontU8.length && locPAMC != 0) {
offset = locPAMC;
let firstChar = data.getUint16(offset, true);
offset += 2;
let lastChar = data.getUint16(offset, true);
offset += 2;
let mapType = data.getUint32(offset, true);
offset += 4;
locPAMC = data.getUint32(offset, true);
offset += 4;
switch(mapType) {
case 0: {
let firstTile = data.getUint16(offset, true);
for(let i = firstChar; i <= lastChar; i++) {
fontMap[firstTile+(i-firstChar)] = i;
}
break;
} case 1: {
for(let i = firstChar; i<= lastChar; i++) {
let tile = data.getUint16(offset, true);
offset += 2;
fontMap[tile] = i;
}
break;
} case 2: {
let groupAmount = data.getUint16(offset, true);
offset += 2;
for(let i = 0; i < groupAmount; i++) {
let charNo = data.getInt16(offset, true);
offset += 2;
let tileNo = data.getInt16(offset, true);
offset += 2;
fontMap[tileNo] = charNo;
}
break;
}
}
}
// Uncomment to log letters in the font map
// let letters = "";
// for(let char of fontMap) {
// letters += String.fromCharCode(char);
// }
// console.log(letters);
document.getElementById("input").style.fontSize = tileWidth + "px";
updateBrush(-1);
for(let i = 0; i < 4; i++) {
updatePalette(i);
}
if(!document.getElementById("editBox").classList.contains("show")) {
new bootstrap.Collapse(document.getElementById("editBox"));
new bootstrap.Collapse(document.getElementById("saveButton"));
}
window.onbeforeunload = function() { return "Are you sure you want to leave? Unsaved data will be lost!"; };
questionMark = 0;
questionMarkChar = "<22>";
questionMark = getCharIndex("<22>");
if(questionMark == 0) {
questionMarkChar = "?";
questionMark = getCharIndex("?");
}
updateBitmap();
}
function saveFont() {
// Copy glyphs back in
let offset = data.getUint32(0x20, true) + 8;
for(let i = 0; i < fontTiles.length; i++) {
fontU8.set(fontTiles[i], offset + (i * tileSize));
}
// Copy widths back in
offset = data.getUint32(0x24, true) + 8;
for(let i = 0; i < fontWidths.length; i++) {
fontU8.set(fontWidths[i], offset + (i * 3));
}
// Update file size
data.setUint32(0x8, fontU8.length, true);
// Download the file
let blob = new Blob([fontU8], {type: "application/octet-stream"});
let a = document.createElement("a");
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
}
function getCharIndex(c, ignoreEncoding = false) {
let char = typeof(c) == "string" ? c.charCodeAt(0) : c;
// If not unicode, convert to shift-jis
if(!ignoreEncoding && encoding != 1) {
let array = Encoding.convert([char], "SJIS");
char = 0;
for(let i = 0; i < array.length; i++) {
char |= array[i] << (8 * (array.length - 1 - i));
}
}
// Try a binary search
let left = 0;
let right = fontMap.length;
while(left <= right) {
let mid = left + ((right - left) / 2);
if(fontMap[mid] == char) {
return mid;
}
if(fontMap[mid] < c) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// If that doesn't find the char, do a linear search
for(let i in fontMap) {
if(fontMap[i] == char) return i;
}
return questionMark;
}
function updateBitmap() {
let str = document.getElementById("input").value;
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x = 0, y = 0;
for(let c of str) {
if(c == '\n') {
y += tileHeight * scale;
x = 0;
continue;
}
let imgData = ctx.createImageData(tileWidth * scale, tileHeight * scale);
let t = getCharIndex(c);
let charImg = new Array(tileHeight * tileWidth);
for(let i = 0; i < tileSize; i++) {
for(let j = 0; j < 8 / tileBitDepth; j++) {
charImg[(i * 8 / tileBitDepth) + j] = (fontTiles[t][i] >> (8 - tileBitDepth) - j * tileBitDepth) & ((1 << tileBitDepth) - 1);
}
}
for(let y = 0; y < tileHeight; y++) {
for(let x = 0; x < tileWidth; x++) {
let sPos = y * tileWidth + x;
for(let i = 0; i < scale; i++) {
let dPos = (y * scale + i) * (tileWidth * scale) + x * scale;
for(let j = 0; j < scale; j++) {
imgData.data[(dPos + j) * 4] = palette[charImg[sPos]][0];
imgData.data[(dPos + j) * 4 + 1] = palette[charImg[sPos]][1];
imgData.data[(dPos + j) * 4 + 2] = palette[charImg[sPos]][2];
imgData.data[(dPos + j) * 4 + 3] = palette[charImg[sPos]][3];
}
}
}
}
let width = ((bytesPerWidth == 3 ? fontWidths[t][2] : fontWidths[t][0] + fontWidths[t][1]) + extraKerning) * scale;
if(x + width > canvas.width) {
y += tileHeight * scale;
x = 0;
}
ctx.putImageData(imgData, x + fontWidths[t][0] * scale, y);
x += width;
}
}
function updatePalette(i) {
let color = document.getElementById("palette" + i).value;
if(color.toUpperCase() == "#FF00FF") {
palette[i] = [0xFF, 0xFF, 0xFF, 0x00];
paletteHTML[i] = "";
} else {
let r = parseInt(color.substr(1, 2), 16);
let g = parseInt(color.substr(3, 2), 16);
let b = parseInt(color.substr(5, 2), 16);
palette[i] = [r, g, b, 0xFF];
paletteHTML[i] = color;
}
if(paletteHTML[i] == "") {
document.getElementById("palette" + i).style.backgroundColor = "gray";
document.getElementById("palette" + i).style.backgroundImage = "repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(255,255,255,.5) 5px, rgba(255,255,255,.5) 10px)";
} else {
document.getElementById("palette" + i).style.backgroundColor = paletteHTML[i];
document.getElementById("palette" + i).style.backgroundImage = "";
}
updateBitmap();
updateBrush(-1);
updateLetterPalette();
}
function clearPalette(i) {
document.getElementById("palette" + i).value = "#FF00FF";
updatePalette(i);
}
function loadLetter() {
let char = document.getElementById("letterInput").value;
let t = 0;
if(char.search(/0x[0-9a-f]+/i) == 0) {
t = getCharIndex(parseInt(char), true);
} else {
t = getCharIndex(char);
}
if(t == questionMark && char[0] != "<22>" && char[0] != "?") {
document.getElementById("letter").innerHTML = "";
document.getElementById("left").value = 0;
document.getElementById("bitmapWidth").value = 0;
document.getElementById("totalWidth").value = 0;
return;
}
let charImg = new Array(tileHeight * tileWidth);
for(let i = 0; i < tileSize; i++) {
for(let j = 0; j < 8 / tileBitDepth; j++) {
charImg[(i * 8 / tileBitDepth) + j] = (fontTiles[t][i] >> (8 - tileBitDepth) - j * tileBitDepth) & ((1 << tileBitDepth) - 1);
}
}
document.getElementById("letter").innerHTML = "";
let row;
for(let y = 0; y < tileHeight; y++) {
row = document.createElement("tr");
for(let x = 0; x < tileWidth; x++) {
let item = document.createElement("td");
item.id = "pixel" + ((y * tileWidth) + x);
item.classList = charImg[(y * tileWidth) + x];
item.style.backgroundColor = paletteHTML[charImg[(y * tileWidth) + x]];
item.onmousedown = function() { drawLetter((y * tileWidth) + x); };
item.onmouseover = function() { drawLetter((y * tileWidth) + x); };
if(x == (fontWidths[t][2] - fontWidths[t][0])) {
item.style.borderLeft = "1px solid red";
} else if(x == fontWidths[t][1]) {
item.style.borderLeft = "1px solid blue";
}
row.appendChild(item);
}
document.getElementById("letter").appendChild(row);
}
// If the last column is colored, apply it to the table itself
if((fontWidths[t][2] - fontWidths[t][0]) == tileWidth) {
document.getElementById("letter").style.borderRight = "1px solid red";
} else if(fontWidths[t][1] == tileWidth) {
document.getElementById("letter").style.borderRight = "1px solid blue";
} else {
document.getElementById("letter").style.borderRight = "";
}
document.getElementById("left").value = fontWidths[t][0];
document.getElementById("left").max = tileWidth;
document.getElementById("bitmapWidth").value = fontWidths[t][1];
document.getElementById("bitmapWidth").max = tileWidth;
document.getElementById("totalWidth").value = fontWidths[t][2];
document.getElementById("totalWidth").max = tileWidth;
}
function updateWidths() {
let t = getCharIndex(document.getElementById("letterInput").value);
for(let i = 0; i < tileWidth * tileHeight; i++) {
if((i % tileWidth) == (document.getElementById("totalWidth").value - document.getElementById("left").value)) {
document.getElementById("pixel" + i).style.borderLeft = "1px solid red";
} else if((i % tileWidth) == document.getElementById("bitmapWidth").value) {
document.getElementById("pixel" + i).style.borderLeft = "1px solid blue";
} else {
document.getElementById("pixel" + i).style.borderLeft = "";
}
}
// If the last column is colored, apply it to the table itself
if((document.getElementById("totalWidth").value - document.getElementById("left").value) == tileWidth) {
document.getElementById("letter").style.borderRight = "1px solid red";
} else if(document.getElementById("bitmapWidth").value == tileWidth) {
document.getElementById("letter").style.borderRight = "1px solid blue";
} else {
document.getElementById("letter").style.borderRight = "";
}
}
function keyListener(on) {
if(on) {
onkeydown = function(e) {
if(e.key == "Shift") {
realColor = brushColor;
updateBrush(0);
} else if(e.key >= 1 && e.key <= 4) {
updateBrush(event.key - 1);
realColor = brushColor;
}
}
onkeyup = function(e) {
if(e.key == "Shift") {
brushColor = realColor;
updateBrush(brushColor);
}
}
} else {
onkeydown = function() {};
onkeyup = function() {};
}
}
function updateBrush(color) {
if(color > -1)
brushColor = color;
for(let i = 0; i < 4; i++) {
document.getElementById("brushColor" + i).style.borderColor = paletteHTML[i] ? paletteHTML[i] : "gray";
if(i == brushColor) {
if(paletteHTML[i] == "") {
document.getElementById("brushColor" + i).style.backgroundColor = "gray";
document.getElementById("brushColor" + i).style.backgroundImage = "repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(255,255,255,.5) 5px, rgba(255,255,255,.5) 10px)";
} else {
document.getElementById("brushColor" + i).style.backgroundColor = paletteHTML[i];
document.getElementById("brushColor" + i).style.backgroundImage = "";
}
} else {
document.getElementById("brushColor" + i).style.backgroundColor = "";
document.getElementById("brushColor" + i).style.backgroundImage = "";
}
}
}
function drawLetter(i) {
let color = brushColor;
if(event.shiftKey) {
color = 0;
}
if(event.buttons) {
document.getElementById("pixel" + i).style.backgroundColor = paletteHTML[color];
document.getElementById("pixel" + i).classList = color;
}
}
function updateLetterPalette() {
if(document.getElementById("letter").hasChildNodes()) {
for(let i = 0; i < tileWidth * tileHeight; i++) {
let color = document.getElementById("pixel" + i).classList[0];
document.getElementById("pixel" + i).style.backgroundColor = paletteHTML[color];
}
}
}
function saveLetter() {
let char = document.getElementById("letterInput").value;
let t = getCharIndex(char);
if(t == questionMark && char[0] != "<22>" && char[0] != "?") return;
for(let i = 0; i < tileWidth * tileHeight; i += (8 / tileBitDepth)) {
let byte = 0;
for(let j = 0; j < (8 / tileBitDepth); j++) {
if(document.getElementById("pixel" + (i + j)))
byte |= (document.getElementById("pixel" + (i + j)).classList[0] & ((1 << tileBitDepth) - 1)) << (8 - (tileBitDepth * (j + 1)));
}
fontTiles[t][i / (8 / tileBitDepth)] = byte;
}
fontWidths[t][0] = document.getElementById("left").value;
fontWidths[t][1] = document.getElementById("bitmapWidth").value;
fontWidths[t][2] = document.getElementById("totalWidth").value;
updateBitmap();
}
function amountToIncrease(increaseAmount, tiles, widths) {
let out = 0;
if(tiles) {
out += increaseAmount * tileSize;
while(out % 4) out++;
}
if(widths) {
out += increaseAmount * bytesPerWidth;
while(out % 4) out++;
}
return out;
}
function addCharacters() {
let str = prompt("Enter the characters you want to add: ");
if(str == null) return;
str = Array.from(str).sort().join("");
let chars = "";
for(let i in str) {
if(str[i] != str[i-1]
&& getCharIndex(str[i]) == questionMark
&& (str[i] != questionMarkChar)
&& str.charCodeAt(i) <= 0xFFFF
&& str.charAt(i) != '\n'
&& str.charAt(i) != '\t') {
chars += str[i];
}
}
console.log("Adding:", chars);
let length = fontU8.length + amountToIncrease(chars.length, true, true);
let newFile = new Uint8Array(length);
let newData = new DataView(newFile.buffer);
let offset = 0x14;
offset += data.getUint32(offset, true);
// Increase chunk size
data.setUint32(offset, data.getUint32(offset, true) + amountToIncrease(chars.length, true, false), true);
// Copy through glyphs
let locHDWC = data.getUint32(0x24, true);
newFile.set(fontU8.subarray(0, locHDWC - 8), 0);
let newLocHDWC = locHDWC + amountToIncrease(chars.length, true, false);
// Increase chunk size
data.setUint32(locHDWC - 4, data.getUint32(locHDWC - 4, true) + amountToIncrease(chars.length, false, true), true);
// Increase HDWC offset
newData.setUint32(0x24, newLocHDWC, true);
// Copy widths
let locPAMC = data.getUint32(0x28, true)
newFile.set(fontU8.subarray(locHDWC - 8, locPAMC - 8), newLocHDWC - 8);
let newLocPAMC = locPAMC + amountToIncrease(chars.length, true, true);
// Increase max character
newData.setUint16(newLocHDWC + 2, newData.getUint16(newLocHDWC + 2, true) + chars.length, true);
// Increase PAMC offset
newData.setUint32(0x28, newLocPAMC, true);
// Copy the rest of the file
newFile.set(fontU8.subarray(locPAMC - 8, fontU8.length), newLocPAMC - 8);
// Increase character maps offsets
while(newLocPAMC <= newFile.length && newData.getUint32(newLocPAMC + 8, true) != 0) {
newData.setUint32(newLocPAMC + 8, newData.getUint32(newLocPAMC + 8, true) + amountToIncrease(chars.length, true, true), true);
newLocPAMC = newData.getUint32(newLocPAMC + 8, true);
}
// Write new size to header
newData.setUint32(8, newFile.length, true);
// Set back to main font buffer
fontU8 = newFile;
// Reload for added bitmaps and widths
reloadFont(fontU8.buffer);
// Add new characters to the end of the map
for(let i = 0; i < chars.length; i++) {
fontMap[fontMap.length - chars.length + i] = chars.charCodeAt(i);
};
// Regenerate the maps
regenMaps();
}
function amountToDecrease(decreaseAmount, tiles, widths) {
let out = 0;
if(tiles) {
out += decreaseAmount * tileSize;
while(out % 4) out++;
}
if(widths) {
out += decreaseAmount * bytesPerWidth;
while(out % 4) out++;
}
return out;
}
function removeCharacters() {
let str = prompt("Enter the characters you want to remove: ");
if(str == null) return;
str = Array.from(str).sort().join("");
let chars = [], indexes = [];
for(let i in str) {
if(str[i] != str[i - 1]
&& (getCharIndex(str[i]) != questionMark || str[i] != "<22>" && str[i] == "?")
&& str.charCodeAt(i) <= 0xFFFF
&& str.charAt(i) != '\n') {
chars.push(str.charCodeAt(i));
indexes.push(fontMap.findIndex(r => r == str.charCodeAt(i)));
}
}
let length = fontU8.length - amountToDecrease(chars.length, true, true);
let newFile = new Uint8Array(length);
let newData = new DataView(newFile.buffer);
let offset = 0x14;
offset += data.getUint32(offset, true);
// Decrease chunk size
data.setUint32(offset, data.getUint32(offset, true) - amountToDecrease(chars.length, true, false), true);
// Copy up to glyphs
let locPLGC = data.getUint32(0x20, true);
newFile.set(fontU8.subarray(0, locPLGC + 8), 0);
// Copy glyphs
for(let i = 0, o = 0; i < fontTiles.length; i++) {
if(!indexes.find(r => r == i)) {
newFile.set(fontU8.subarray(locPLGC + 8 + (i * fontTiles[0].length), locPLGC + 8 + ((i + 1) * fontTiles[0].length)), locPLGC + 8 + (o++ * fontTiles[0].length));
}
}
let locHDWC = data.getUint32(0x24, true);
let newLocHDWC = locHDWC - amountToDecrease(chars.length, true, false);
// Decrease chunk size
data.setUint32(locHDWC - 4, data.getUint32(locHDWC - 4, true) - amountToDecrease(chars.length, false, true), true);
// Decrease HDWC offset
newData.setUint32(0x24, newLocHDWC, true);
// Copy widths header
newFile.set(fontU8.subarray(locHDWC - 8, locHDWC + 8), newLocHDWC - 8);
// Copy widths
for(let i = 0, o = 0; i < fontWidths.length; i++) {
if(!indexes.find(r => r == i)) {
newFile.set(fontU8.subarray(locHDWC + 8 + (i * bytesPerWidth), locHDWC + 8 + ((i + 1) * bytesPerWidth)), newLocHDWC + 8 + (o++ * bytesPerWidth));
}
}
let locPAMC = data.getUint32(0x28, true);
let newLocPAMC = locPAMC - amountToDecrease(chars.length, true, true);
// Increase PAMC offset
newData.setUint32(0x28, newLocPAMC, true);
// Copy the rest of the file
newFile.set(fontU8.subarray(locPAMC - 8, fontU8.length), newLocPAMC - 8);
// Decrease character maps offsets
while(newLocPAMC <= newFile.length && newData.getUint32(newLocPAMC + 8, true) != 0) {
let final = newData.getUint32(newLocPAMC + 4, true) == 2;
newData.setUint32(newLocPAMC + 8, newData.getUint32(newLocPAMC + 8, true) - amountToDecrease(chars.length, true, true) + (final ? chars.length * 4 : 0), true);
newLocPAMC = newData.getUint32(newLocPAMC + 8, true);
}
// Write new size to header
newData.setUint32(8, newFile.length, true);
// Set back to main font buffer
fontU8 = newFile;
data = newData;
// Reload for added bitmaps and widths
reloadFont(fontU8.buffer);
// Remove characters from the map
fontMap = fontMap.filter(r => !chars.find(x => x == r));
// Regenerate the maps
regenMaps();
// Decrease max character
data.setUint16(newLocHDWC + 2, newData.getUint16(newLocHDWC + 2, true) - chars.length, true);
reloadFont(fontU8.buffer);
}
function generateFromFont() {
let ctx = document.createElement("canvas").getContext("2d"); // Create canvas context
ctx.canvas.width = tileWidth;
ctx.canvas.height = tileHeight;
let regen = confirm("Regerate existing characters?\n\nCancel = No, OK = Yes");
let regenButtons = regen ? confirm("Regenerate special button characters? (Only in Nintendo's font)\n\nCancel = No, OK = Yes") : false;
let font = document.getElementById("inputFont").value;
let bold = document.getElementById("fontWeight").checked ? "bold " : "";
let italic = document.getElementById("fontStyle").checked ? "italic " : "";
if(font == "")
font = "Sans-Serif";
ctx.font = bold + italic + tileWidth + "px " + font;
let maxDifference = document.getElementById("maxDifference").value;
if(maxDifference == 0)
maxDifference = Infinity;
for(let i in fontMap) {
if((!regen && !fontTiles[i].every(function(x) { return x == fontTiles[i][0]; }))
|| (!regenButtons && fontMap[i] >= 0xE000 && fontMap[i] <= 0xE07E))
continue;
let char = String.fromCharCode(fontMap[i]);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillText(char, 0, tileWidth);
let image = ctx.getImageData(0, 0, tileWidth, tileHeight);
let newBitmap = [];
for(let i = 0; i < image.data.length; i += 4) {
newBitmap.push(palette.indexOf(palette.reduce((prev, cur) => {
if(Math.abs((0xFF - image.data[i + 3]) - cur[0]) > maxDifference)
return prev;
return Math.abs((0xFF - image.data[i + 3]) - cur[0]) < Math.abs((0xFF - image.data[i + 3]) - prev[0]) ? cur : prev;
})));
}
let t = getCharIndex(char);
if(t == questionMark && char[0] != "<22>" && char != "?") continue;
for(let i = 0; i < tileWidth * tileHeight; i += 4) {
let byte = 0;
byte |= (newBitmap[i] & 3) << 6;
byte |= (newBitmap[i + 1] & 3) << 4;
byte |= (newBitmap[i + 2] & 3) << 2;
byte |= (newBitmap[i + 3] & 3) << 0;
fontTiles[t][i / 4] = byte;
}
fontWidths[t][0] = 0;
fontWidths[t][1] = Math.min(Math.round(ctx.measureText(char).width), tileWidth);
fontWidths[t][2] = fontWidths[t][1];
}
updateBitmap();
}
function updateFont() {
document.getElementById("input").style.fontFamily = document.getElementById("inputFont").value;
document.getElementById("letterInput").style.fontFamily = document.getElementById("inputFont").value;
document.getElementById("input").style.fontWeight = document.getElementById("fontWeight").checked ? "bold" : "normal";
document.getElementById("letterInput").style.fontWeight = document.getElementById("fontWeight").checked ? "bold" : "normal";
document.getElementById("input").style.fontStyle = document.getElementById("fontStyle").checked ? "italic" : "normal";
document.getElementById("letterInput").style.fontStyle = document.getElementById("fontStyle").checked ? "italic" : "normal";
}
function exportImage() {
let columns = parseInt(prompt("How many columns do you want?", "32"));
let padding = parseInt(prompt("How much padding do you want? (in pixels)", "0"));
if(isNaN(columns) || isNaN(padding))
return;
let ctx = document.createElement("canvas").getContext("2d"); // Create canvas context
ctx.canvas.width = (tileWidth + padding) * columns - padding;
ctx.canvas.height = (tileHeight + padding) * Math.ceil(fontMap.length / columns) - padding;
ctx.beginPath();
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "#f2acae";
ctx.fill();
let x = 0, y = 0;
for(let c in fontMap) {
let imgData = ctx.createImageData(tileWidth, tileHeight);
let charImg = new Array(tileHeight * tileWidth);
for(let i = 0; i < tileSize; i++) {
for(let j = 0; j < 8 / tileBitDepth; j++) {
charImg[(i * 8 / tileBitDepth) + j] = (fontTiles[c][i] >> (8 - tileBitDepth) - j * tileBitDepth) & ((1 << tileBitDepth) - 1);
}
}
for(let i = 0; i < imgData.data.length / 4; i++) {
imgData.data[i * 4] = palette[charImg[i]][0];
imgData.data[i * 4 + 1] = palette[charImg[i]][1];
imgData.data[i * 4 + 2] = palette[charImg[i]][2];
imgData.data[i * 4 + 3] = palette[charImg[i]][3];
}
ctx.putImageData(imgData, x, y);
x += tileWidth + padding;
if(x >= ctx.canvas.width) {
y += tileHeight + padding;
x = 0;
}
}
// Download the file
let binString = atob(ctx.canvas.toDataURL().split(',')[1]);
let arrBuf = new ArrayBuffer(binString.length);
let arr = new Uint8Array(arrBuf);
for(let i in binString) {
arr[i] = binString.charCodeAt(i);
}
let blob = new Blob([arrBuf], {type: "image/png"});
let a = document.createElement('a');
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName + ".png";
a.click();
window.URL.revokeObjectURL(url);
}
function importImage(file) {
let padding = parseInt(prompt("How much padding was used when exporting? (in pixels)", "0"));
if(isNaN(padding))
return;
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
let image = new Image();
image.src = this.result;
image.onload = function() {
let columns = (this.width + padding) / (tileWidth + padding);
let ctx = document.createElement("canvas").getContext("2d"); // Create canvas context
ctx.canvas.width = (tileWidth + padding) * columns - padding;
ctx.canvas.height = (tileHeight + padding) * Math.ceil(fontMap.length / columns) - padding;
ctx.drawImage(this, 0, 0);
if(this.width != ctx.canvas.width || this.height != ctx.canvas.height) {
alert("Wrong image/padding size!");
return;
}
for(let c in fontMap) {
let image = ctx.getImageData((c % columns) * (tileWidth + padding), Math.floor(c / columns) * (tileHeight + padding), tileWidth, tileHeight);
let newBitmap = [];
for(let i = 0; i < image.data.length; i += 4) {
newBitmap.push(palette.indexOf(palette.reduce((prev, cur) => {
// If transparent, force transparent
if(image.data[i + 3] < 255) {
if(cur[3] == 0)
return cur;
else if(prev[3] == 0)
return prev;
}
let cres = 0, pres = 0;
for(let j = 0; j < 4; j++) {
cres += Math.abs(image.data[i + j] - cur[j]);
pres += Math.abs(image.data[i + j] - prev[j]);
}
return cres / palette.length < pres / palette.length ? cur : prev;
})));
}
for(let i = 0; i < tileWidth * tileHeight; i += (8 / tileBitDepth)) {
let byte = 0;
for(let j = 0; j < (8 / tileBitDepth); j++) {
byte |= (newBitmap[i + j] & ((1 << tileBitDepth) - 1)) << (8 - (tileBitDepth * (j + 1)));
}
fontTiles[c][i / (8 / tileBitDepth)] = byte;
}
}
updateBitmap();
}
}
}
function exportSizes() {
let out = [];
for(let c of fontMap) {
let orig = c;
// If not unicode, convert from shift-jis
if(encoding != 1) {
let array = [];
do {
array.push(c & 0xFF);
c = c >> 8;
} while(c > 0);
array.reverse();
c = Encoding.convert(array, "UNICODE", "SJIS")[0];
}
c = String.fromCharCode(c);
let i = getCharIndex(c);
out.push(bytesPerWidth == 3 ? {
"char": (c == "?" && c.charCodeAt(0) != orig) ? `0x${orig.toString(16).padStart(4, "0")} (Shift-JIS)` : c,
"left spacing": fontWidths[i][0],
"bitmap width": fontWidths[i][1],
"total width": fontWidths[i][2]
} : {
"char": (c == "?" && c.charCodeAt(0) != orig) ? `0x${orig.toString(16).padStart(4, "0")} (Shift-JIS)` : c,
"left spacing": fontWidths[i][0],
"bitmap width": fontWidths[i][1]
});
}
// Download the file
let blob = new Blob([JSON.stringify(out, null, 2)], {type: "application/json"});
let a = document.createElement('a');
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName + ".json";
a.click();
window.URL.revokeObjectURL(url);
}
function importSizes(file) {
let reader = new FileReader();
reader.readAsText(file);
reader.onload = function() {
let json = JSON.parse(this.result);
for(let char of json) {
let i = getCharIndex(char["char"]);
fontWidths[i][0] = char["left spacing"];
fontWidths[i][1] = char["bitmap width"];
fontWidths[i][2] = char["total width"];
}
updateBitmap();
}
}
function sortMaps() {
let maps = [];
// Make combined map
for(let i = 0; i < fontMap.length; i++) {
maps.push({"map": fontMap[i], "tile": fontTiles[i], "width": fontWidths[i]})
}
// Sort by character mappings
let sorted = maps.sort(function(l, r) {
if(l.map < r.map) return -1;
else if(l.map > r.map) return 1;
return 0;
});
// Split back out
for(let i = 0; i < fontMap.length; i++) {
fontMap[i] = sorted[i].map;
fontTiles[i] = sorted[i].tile;
fontWidths[i] = sorted[i].width;
}
// Copy back to font
let offset = data.getUint32(0x20, true) + 8;
for(let i = 0; i < fontTiles.length; i++) {
fontU8.set(fontTiles[i], offset + (i * fontTiles[0].length));
}
offset = data.getUint32(0x24, true) + 8;
for(let i = 0; i < fontWidths.length; i++) {
fontU8.set(fontWidths[i], offset + (i * bytesPerWidth));
}
}
function regenMaps() {
let maps = [], last = [0, fontMap[0]], range = [[fontMap[0], 0]];
sortMaps();
for(let c = 1; c <= fontMap.length; c++) {
if(fontMap[c] - 1 == last[1]) {
range.push([fontMap[c], c]);
} else {
maps.push(range);
if(c < fontMap.length)
range = [[fontMap[c], c]];
}
last = [c, fontMap[c]];
}
let type2 = [];
maps.filter(r => r.length <= 0x14).forEach(r => r.forEach(item => type2.push(item)));
maps = maps.filter(r => r.length > 0x14);
let ofs = data.getUint32(0x28, true) - 8;
outMaps = [];
maps.forEach(function(r) {
outMaps.push(new CharMap0(r[0][0], r[r.length - 1][0], ofs, r[0][1]))
ofs = outMaps[outMaps.length - 1].offset + outMaps[outMaps.length - 1].length;
});
outMaps.push(new CharMap2(0, 0xFFFF, ofs, type2));
let newU8 = new Uint8Array(outMaps[outMaps.length - 1].offset + outMaps[outMaps.length - 1].length);
let newData = new DataView(newU8.buffer);
// Copy through maps
ofs = data.getUint32(0x28, true) - 8;
newU8.set(fontU8.subarray(0, ofs), 0);
// Set new size in header
newData.setUint32(0x8, newU8.length, true);
// Set new chunk count
newData.setUint16(0xE, 0x3 + outMaps.length, true);
outMaps.forEach(r => newU8.set(r.get(), r.offset));
fontU8 = newU8;
data = newData;
// Reload font
reloadFont(fontU8.buffer);
}
function pad(num, length, base = 10) {
let str = num.toString(base);
while(str.length < length) {
str = "0" + str;
}
return str;
}
DataView.prototype.setString = function(byteOffset, value) {
if(typeof(byteOffset) == "number" && typeof(value) == "string") {
for(let c = 0; c < value.length; c++) {
this.setUint8(byteOffset + c, value.charCodeAt(c));
}
}
}
class CharMap0 {
// Type 0
constructor(firstChar, lastChar, offset, firstTile) {
if(typeof(firstChar) != "number")
return console.error("Type error! Should be 'number'", firstChar);
this.firstChar = firstChar;
if(typeof(lastChar) != "number")
return console.error("Type error! Should be 'number'", lastChar);
this.lastChar = lastChar;
if(typeof(offset) != "number")
return console.error("Type error! Should be 'number'", offset);
this.offset = offset;
if(typeof(firstTile) != "number")
return console.error("Type error! Should be 'number'", firstTile);
this.firstTile = firstTile;
this.length = 0x18;
}
get() {
let arr = new Uint8Array(0x18);
let data = new DataView(arr.buffer);
data.setString(0x00, "PAMC"); // ID
data.setUint32(0x04, this.length, true); // Chunk size
data.setUint16(0x08, this.firstChar, true); // First char
data.setUint16(0x0A, this.lastChar, true); // Last char
data.setUint32(0x0C, 0, true); // Map type
data.setUint32(0x10, this.offset + 0x18 + 8, true); // Offset to next
data.setUint16(0x14, this.firstTile, true); // First tile no
return arr;
}
toString() {
let str = "";
this.get().forEach(r => str += pad(r, 2, 16));
return str;
}
}
// TODO: Char map 1
class CharMap2 {
// Type 2
constructor(firstChar, lastChar, offset, pairs) {
if(typeof(firstChar) != "number")
return console.error("Type error! Should be 'number'", firstChar);
this.firstChar = firstChar;
if(typeof(lastChar) != "number")
return console.error("Type error! Should be 'number'", lastChar);
this.lastChar = lastChar;
if(typeof(offset) != "number")
return console.error("Type error! Should be 'number'", offset);
this.offset = offset;
if(typeof(pairs) != "object")
return console.error("Type error! Should be 'object'", pairs);
this.pairs = pairs;
this.length = 0x14 + 2 + (this.pairs.length * 4) + 2;
}
get(zeroForNext = true) {
let arr = new Uint8Array(this.length);
let data = new DataView(arr.buffer);
data.setString(0x00, "PAMC"); // ID
data.setUint32(0x04, this.length, true); // Chunk size
data.setUint16(0x08, this.firstChar, true); // First char
data.setUint16(0x0A, this.lastChar, true); // Last char
data.setUint32(0x0C, 2, true); // Map type
data.setUint32(0x10, zeroForNext ? 0 : this.offset + this.length + 8, true); // Offset to next
data.setUint16(0x14, this.pairs.length, true); // Number of char=tile pairs
for(let i = 0; i < this.pairs.length; i++) {
data.setUint16(0x16 + (i * 4), this.pairs[i][0], true); // Char no
data.setUint16(0x18 + (i * 4), this.pairs[i][1], true); // Tile no
}
return arr;
}
toString() {
let str = "";
this.get().forEach(r => str += pad(r, 2, 16));
return str;
}
}
function resize(width, height) {
if(typeof(width) != "number")
width = parseInt(prompt("Enter the new width:", tileWidth));
if(typeof(height) != "number")
height = parseInt(prompt("Enter the new height:", tileHeight));
if(isNaN(width) || isNaN(height))
return alert("Please enter two numbers!");
let oldTileWidth = tileWidth, oldTileHeight = tileHeight, oldTileSize = tileSize;
tileWidth = width;
tileHeight = height;
tileSize = Math.floor((tileWidth * tileHeight * 2 + 7) / 8);
tileSize += (4 - tileSize % 4) % 4;
let decreaseAmount = (oldTileSize - tileSize) * fontTiles.length;
// Change font info sizes
data.setUint8(0x19, tileHeight);
data.setUint8(0x1D, tileWidth);
data.setUint8(0x1E, tileWidth);
if(data.getUint32(0x14, true) == 0x20) {
data.setUint8(0x2C, tileHeight);
data.setUint8(0x2D, tileWidth);
}
// Decrease chunk size
let offset = 0x14 + data.getUint8(0x14);
data.setUint32(offset, data.getUint32(offset, true) - decreaseAmount, true);
// Change glyph info sizes
data.setUint8(offset + 4, tileWidth);
data.setUint8(offset + 5, tileHeight);
data.setUint16(offset + 6, tileSize, true);
data.setUint8(offset + 9, tileWidth + 1);
// Resize tiles
if(confirm("Scale the text?")) {
let canvas = document.createElement("canvas");
for(let t in fontTiles) {
let ctx = canvas.getContext("2d");
let scaleCtx = document.createElement("canvas").getContext("2d");
scaleCtx.scale(tileWidth / oldTileWidth, tileHeight / oldTileHeight);
let imgData = ctx.createImageData(oldTileWidth, oldTileHeight);
let charImg = new Array(oldTileWidth * oldTileHeight);
for(let i = 0; i < oldTileSize; i++) {
for(let j = 0; j < 8 / tileBitDepth; j++) {
charImg[(i * 8 / tileBitDepth) + j] = (fontTiles[t][i] >> (8 - tileBitDepth) - j * tileBitDepth) & ((1 << tileBitDepth) - 1);
}
}
for(let i = 0; i < imgData.data.length / 4; i++) {
imgData.data[i * 4] = palette[charImg[i]][0];
imgData.data[i * 4 + 1] = palette[charImg[i]][1];
imgData.data[i * 4 + 2] = palette[charImg[i]][2];
imgData.data[i * 4 + 3] = palette[charImg[i]][3];
}
// Scale to new size
ctx.putImageData(imgData, 0, 0);
scaleCtx.drawImage(canvas, 0, 0);
let image = scaleCtx.getImageData(0, 0, tileWidth, tileHeight);
let newBitmap = [];
for(let i = 0; i < image.data.length; i += 4) {
newBitmap.push(palette.indexOf(palette.reduce((prev, cur) => {
return Math.abs((0xFF - image.data[i + 3]) - cur[0]) < Math.abs((0xFF - image.data[i + 3]) - prev[0]) ? cur : prev;
})));
}
fontTiles[t] = new Uint8Array(tileSize);
for(let i = 0; i < tileWidth * tileHeight; i += 4) {
let byte = 0;
byte |= (newBitmap[i] & 3) << 6;
byte |= (newBitmap[i + 1] & 3) << 4;
byte |= (newBitmap[i + 2] & 3) << 2;
byte |= (newBitmap[i + 3] & 3) << 0;
fontTiles[t][i / 4] = byte;
}
}
// Scale widths
for(let i in fontWidths) {
for(let j in fontWidths[i]) {
fontWidths[i][j] = Math.round(fontWidths[i][j] * tileWidth / oldTileWidth);
}
}
} else {
for(let t in fontTiles) {
let tile = new Uint8Array(tileSize);
for(let y = 0; y < oldTileHeight; y++) {
for(let x = 0; x < oldTileWidth; x++) {
let px = (fontTiles[t][Math.floor((y * oldTileWidth + x) / (8 / tileBitDepth))] >> (8 - tileBitDepth) - ((y * oldTileWidth + x) % (8 / tileBitDepth)) * tileBitDepth) & ((1 << tileBitDepth) - 1);
tile[Math.floor((y * tileWidth + x) / (8 / tileBitDepth))] = (tile[Math.floor((y * tileWidth + x) / (8 / tileBitDepth))] & ~(((1 << tileBitDepth) - 1) << (8 - tileBitDepth) - ((y * tileWidth + x) % (8 / tileBitDepth)) * tileBitDepth)) | (px << (8 - tileBitDepth) - ((y * tileWidth + x) % (8 / tileBitDepth)) * tileBitDepth);
}
}
fontTiles[t] = tile;
}
}
// Reduce offsets
let locHDWC = data.getUint32(0x24, true) - decreaseAmount;
data.setUint32(0x24, locHDWC, true);
let locPAMC = 0x28 - 8;
while(locPAMC < fontU8.length && locPAMC != 0) {
let old = data.getUint32(locPAMC + 8, true);
if(old == 0)
break;
data.setUint32(locPAMC + 8, old - decreaseAmount, true);
locPAMC = old;
}
// Remove unused section
let newFile = new Uint8Array(fontU8.length - decreaseAmount);
newFile.set(fontU8.subarray(0, locHDWC - 8), 0);
newFile.set(fontU8.subarray(locHDWC + decreaseAmount - 8), locHDWC - 8);
fontU8 = newFile;
data = new DataView(fontU8.buffer);
// Change the font size of the input box
document.getElementById("input").style.fontSize = tileWidth + "px";
updateBitmap();
}
// Remove a specific broken character
function rmAt(index) {
let chars = [fontMap[index]], indexes = [index];
let length = fontU8.length - amountToDecrease(chars.length, true, true);
let newFile = new Uint8Array(length);
let newData = new DataView(newFile.buffer);
let offset = 0x14;
offset += data.getUint32(offset, true);
// Decrease chunk size
data.setUint32(offset, data.getUint32(offset, true) - amountToDecrease(chars.length, true, false), true);
// Copy up to glyphs
let locPLGC = data.getUint32(0x20, true);
newFile.set(fontU8.subarray(0, locPLGC + 8), 0);
// Copy glyphs
for(let i = 0, o = 0; i < fontTiles.length; i++) {
if(indexes[0] != i) {
newFile.set(fontU8.subarray(locPLGC + 8 + (i * fontTiles[0].length), locPLGC + 8 + ((i + 1) * fontTiles[0].length)), locPLGC + 8 + (o++ * fontTiles[0].length));
}
}
let locHDWC = data.getUint32(0x24, true);
let newLocHDWC = locHDWC - amountToDecrease(chars.length, true, false);
// Decrease chunk size
data.setUint32(locHDWC - 4, data.getUint32(locHDWC - 4, true) - amountToDecrease(chars.length, false, true), true);
// Decrease HDWC offset
newData.setUint32(0x24, newLocHDWC, true);
// Copy widths header
newFile.set(fontU8.subarray(locHDWC - 8, locHDWC + 8), newLocHDWC - 8);
// Copy widths
for(let i = 0, o = 0; i < fontWidths.length; i++) {
if(indexes[0] != i) {
newFile.set(fontU8.subarray(locHDWC + 8 + (i * 3), locHDWC + 8 + ((i + 1) * 3)), newLocHDWC + 8 + (o++ * 3));
}
}
let locPAMC = data.getUint32(0x28, true);
let newLocPAMC = locPAMC - amountToDecrease(chars.length, true, true);
// Increase PAMC offset
newData.setUint32(0x28, newLocPAMC, true);
// Copy the rest of the file
newFile.set(fontU8.subarray(locPAMC - 8, fontU8.length), newLocPAMC - 8);
// Decrease character maps offsets
while(newLocPAMC <= newFile.length && newData.getUint32(newLocPAMC + 8, true) != 0) {
let final = newData.getUint32(newLocPAMC + 4, true) == 2;
newData.setUint32(newLocPAMC + 8, newData.getUint32(newLocPAMC + 8, true) - amountToDecrease(chars.length, true, true) + (final ? chars.length * 4 : 0), true);
newLocPAMC = newData.getUint32(newLocPAMC + 8, true);
}
// Write new size to header
newData.setUint32(8, newFile.length, true);
// Set back to main font buffer
fontU8 = newFile;
data = newData;
// Reload for added bitmaps and widths
reloadFont(fontU8.buffer);
// Remove character from the map
newMap = new Uint16Array(fontMap.length - 1);
newMap.set(fontMap.subarray(0, indexes[0]), 0);
newMap.set(fontMap.subarray(indexes[0] + 1, fontMap.length), indexes[0]);
fontMap = newMap
// Regenerate the maps
regenMaps();
// Decrease max character
data.setUint16(newLocHDWC + 2, newData.getUint16(newLocHDWC + 2, true) - chars.length, true);
reloadFont(fontU8.buffer);
}
function updateExtraKerning(event) {
extraKerning = parseInt(document.getElementById("extraKerning").value) || 0;
updateBitmap();
}
function setBg(value) {
if(value[0] == "#") {
document.getElementById("canvas").style.backgroundColor = value;
document.getElementById("canvas").style.backgroundImage = "";
document.getElementById("bgColor").style.backgroundColor = value
} else {
document.getElementById("canvas").style.backgroundColor = "";
document.getElementById("canvas").style.backgroundImage = "url(" + value.replace(/[ ()]/g, r => "%" + r.charCodeAt(0).toString(16)) + ")";
document.getElementById("bgColor").style.backgroundColor = ""
}
}
function setScale(value) {
scale = value | 0;
updateBitmap();
}