2017-09-08 09:24:16 -07:00
|
|
|
//
|
|
|
|
// nftr.js
|
|
|
|
//--------------------
|
|
|
|
// Reads NFTR fonts and compiles them to a texture and character lookup table. Texture is replaceable.
|
|
|
|
// by RHY3756547
|
|
|
|
//
|
|
|
|
// includes: gl-matrix.js (glMatrix 2.0)
|
|
|
|
// /formats/nitro.js
|
|
|
|
//
|
|
|
|
|
|
|
|
window.nftr = function(input) {
|
|
|
|
|
|
|
|
var mainOff;
|
2019-06-09 15:50:55 -07:00
|
|
|
var t = this;
|
|
|
|
this.info = {};
|
2017-09-08 09:24:16 -07:00
|
|
|
|
|
|
|
if (input != null) {
|
|
|
|
load(input);
|
|
|
|
}
|
|
|
|
this.load = load;
|
2019-06-09 15:50:55 -07:00
|
|
|
this.drawToCanvas = drawToCanvas;
|
|
|
|
this.measureText = measureText;
|
|
|
|
this.measureMapped = measureMapped;
|
|
|
|
this.mapText = mapText;
|
2017-09-08 09:24:16 -07:00
|
|
|
|
|
|
|
function load(input) {
|
|
|
|
var view = new DataView(input);
|
|
|
|
var header = null;
|
|
|
|
var offset = 0;
|
|
|
|
var tex;
|
|
|
|
|
|
|
|
//nitro 3d header
|
|
|
|
header = nitro.readHeader(view);
|
|
|
|
//debugger;
|
|
|
|
if (header.stamp != "RTFN") throw "NFTR invalid. Expected RTFN, found "+header.stamp;
|
2019-06-09 15:50:55 -07:00
|
|
|
offset = 0x10; //nitro header for nftr doesn't have section offsets - they are in order
|
2017-09-08 09:24:16 -07:00
|
|
|
//end nitro
|
2019-06-09 15:50:55 -07:00
|
|
|
|
|
|
|
var info = t.info;
|
|
|
|
info.type = readChar(view, offset+0x0)+readChar(view, offset+0x1)+readChar(view, offset+0x2)+readChar(view, offset+0x3);
|
|
|
|
info.blockSize = view.getUint32(offset+0x4, true);
|
|
|
|
|
|
|
|
info.unknown1 = view.getUint8(offset+0x8);
|
|
|
|
info.height = view.getUint8(offset+0x9);
|
|
|
|
info.nullCharIndex = view.getUint16(offset+0xA, true);
|
|
|
|
info.unknown2 = view.getUint8(offset+0xC);
|
|
|
|
info.width = view.getUint8(offset+0xD);
|
|
|
|
info.widthBis = view.getUint8(offset+0xE);
|
|
|
|
info.encoding = view.getUint8(offset+0xF);
|
|
|
|
|
|
|
|
info.offsetCGLP = view.getUint32(offset+0x10, true); //character graphics
|
|
|
|
info.offsetCWDH = view.getUint32(offset+0x14, true); //character width
|
|
|
|
info.offsetCMAP = view.getUint32(offset+0x18, true); //character map
|
|
|
|
|
|
|
|
if (info.blockSize == 0x20) {
|
|
|
|
//extra info
|
|
|
|
info.fontHeight = view.getUint8(offset+0x1C);
|
|
|
|
info.fontWidth = view.getUint8(offset+0x1D);
|
|
|
|
info.bearingX = view.getUint8(offset+0x1E);
|
|
|
|
info.bearingY = view.getUint8(offset+0x1F);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadCGLP(view);
|
|
|
|
loadCWDH(view);
|
|
|
|
loadCMAP(view);
|
2017-09-08 09:24:16 -07:00
|
|
|
|
|
|
|
mainOff = offset;
|
|
|
|
}
|
2019-06-09 15:50:55 -07:00
|
|
|
|
|
|
|
function loadCGLP(view) {
|
|
|
|
var offset = t.info.offsetCGLP - 8;
|
|
|
|
|
|
|
|
var cglp = {};
|
|
|
|
cglp.type = readChar(view, offset+0x0)+readChar(view, offset+0x1)+readChar(view, offset+0x2)+readChar(view, offset+0x3);
|
|
|
|
cglp.blockSize = view.getUint32(offset+0x4, true);
|
|
|
|
cglp.tileWidth = view.getUint8(offset+0x8);
|
|
|
|
cglp.tileHeight = view.getUint8(offset+0x9);
|
|
|
|
cglp.tileLength = view.getUint16(offset+0xA, true);
|
|
|
|
cglp.unknown = view.getUint16(offset+0xC, true);
|
|
|
|
cglp.depth = view.getUint8(offset+0xE);
|
|
|
|
cglp.rotateMode = view.getUint8(offset+0xF);
|
|
|
|
|
|
|
|
offset += 0x10;
|
|
|
|
cglp.tiles = [];
|
|
|
|
var total = (cglp.blockSize - 0x10) / cglp.tileLength;
|
|
|
|
for (var i=0; i<total; i++) {
|
|
|
|
cglp.tiles.push(new Uint8Array(view.buffer.slice(offset, offset+cglp.tileLength)));
|
|
|
|
offset += cglp.tileLength;
|
|
|
|
}
|
|
|
|
t.cglp = cglp;
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadCWDH(view) {
|
|
|
|
var offset = t.info.offsetCWDH - 8;
|
|
|
|
|
|
|
|
var cwdh = {};
|
|
|
|
cwdh.type = readChar(view, offset+0x0)+readChar(view, offset+0x1)+readChar(view, offset+0x2)+readChar(view, offset+0x3);
|
|
|
|
cwdh.blockSize = view.getUint32(offset+0x4, true);
|
|
|
|
cwdh.firstCode = view.getUint16(offset+0x8, true);
|
|
|
|
cwdh.lastCode = view.getUint16(offset+0xA, true);
|
|
|
|
|
|
|
|
cwdh.unknown = view.getUint32(offset+0xC, true);
|
|
|
|
|
|
|
|
cwdh.info = [];
|
|
|
|
offset += 0x10;
|
|
|
|
for (var i=0; i<t.cglp.tiles.length; i++) {
|
|
|
|
var info = {};
|
|
|
|
info.pixelStart = view.getInt8(offset);
|
|
|
|
info.pixelWidth = view.getUint8(offset+1);
|
|
|
|
info.pixelLength = view.getUint8(offset+2);
|
|
|
|
cwdh.info.push(info);
|
|
|
|
offset += 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
t.cwdh = cwdh;
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadCMAP(view) {
|
|
|
|
var offset = t.info.offsetCMAP - 8;
|
|
|
|
var cmaps = [];
|
|
|
|
var charMap = {};
|
|
|
|
while (offset > 0 && offset < view.byteLength) {
|
|
|
|
var cmap = {};
|
|
|
|
cmap.type = readChar(view, offset+0x0)+readChar(view, offset+0x1)+readChar(view, offset+0x2)+readChar(view, offset+0x3);
|
|
|
|
cmap.blockSize = view.getUint32(offset+0x4, true);
|
|
|
|
cmap.firstChar = view.getUint16(offset+0x8, true);
|
|
|
|
cmap.lastChar = view.getUint16(offset+0xA, true);
|
|
|
|
|
|
|
|
cmap.typeSection = view.getUint32(offset+0xC, true);
|
|
|
|
cmap.nextOffset = view.getUint32(offset+0x10, true);
|
|
|
|
|
|
|
|
offset += 0x14;
|
|
|
|
switch (cmap.typeSection & 0xFFFF) {
|
|
|
|
case 1: //char code list (first to last)
|
|
|
|
cmap.charCodes = [];
|
|
|
|
var total = (cmap.lastChar - cmap.firstChar) + 1;
|
|
|
|
var charCode = cmap.firstChar;
|
|
|
|
for (var i=0; i<total; i++) {
|
|
|
|
var char = view.getUint16(offset, true);
|
|
|
|
cmap.charCodes.push(char);
|
|
|
|
if (char != 65535) {
|
|
|
|
charMap[String.fromCharCode(charCode)] = char;
|
|
|
|
}
|
|
|
|
charCode++;
|
|
|
|
offset += 2;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 2: //char code map
|
|
|
|
cmap.numChars = view.getUint16(offset, true);
|
|
|
|
offset += 2;
|
|
|
|
cmap.charMap = [];
|
|
|
|
for (var i=0; i<cmap.numChars; i++) {
|
|
|
|
var charCode = view.getUint16(offset, true);
|
|
|
|
var char = view.getUint16(offset+2, true);
|
|
|
|
cmap.charMap.push([charCode, char]);
|
|
|
|
charMap[String.fromCharCode(charCode)] = char;
|
|
|
|
offset += 4;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
cmap.firstCharCode = view.getUint16(offset, true);
|
|
|
|
var total = (cmap.lastChar - cmap.firstChar) + 1;
|
|
|
|
var charCode = cmap.firstChar;
|
|
|
|
var char = cmap.firstCharCode;
|
|
|
|
|
|
|
|
for (var i=0; i<total; i++) {
|
|
|
|
charMap[String.fromCharCode(charCode++)] = char++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
cmaps.push(cmap);
|
|
|
|
offset = cmap.nextOffset - 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
t.charMap = charMap;
|
|
|
|
t.cmaps = cmaps;
|
|
|
|
}
|
|
|
|
|
|
|
|
function readChar(view, offset) {
|
|
|
|
return String.fromCharCode(view.getUint8(offset));
|
|
|
|
}
|
|
|
|
|
|
|
|
// RENDERING FUNCTIONS
|
|
|
|
|
|
|
|
function mapText(text, missing) {
|
|
|
|
if (missing == null) missing = "*";
|
|
|
|
var map = t.charMap;
|
|
|
|
var result = [];
|
|
|
|
for (var i=0; i<text.length; i++) {
|
|
|
|
var code = text[i];
|
|
|
|
var mapped = map[code];
|
|
|
|
if (mapped == null) mapped = map[missing];
|
|
|
|
result.push(mapped);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function measureText(text, missing, spacing) {
|
|
|
|
return measureMapped(mapText(text, missing), spacing);
|
|
|
|
}
|
|
|
|
|
|
|
|
function measureMapped(mapped, spacing) {
|
|
|
|
if (spacing == null) spacing = 1;
|
|
|
|
var width = 0;
|
|
|
|
var widths = t.cwdh.info;
|
|
|
|
|
|
|
|
for (var i=0; i<mapped.length; i++) {
|
|
|
|
width += widths[mapped[i]].pixelLength + spacing; // pixelWidth is the width of drawn section - length is how wide the char is
|
|
|
|
}
|
|
|
|
|
|
|
|
return [width, t.info.height];
|
|
|
|
}
|
|
|
|
|
|
|
|
function drawToCanvas(text, palette, spacing) {
|
|
|
|
if (spacing == null) spacing = 1;
|
|
|
|
var mapped = mapText(text, "");
|
|
|
|
var size = measureMapped(mapped, spacing);
|
|
|
|
|
|
|
|
var canvas = document.createElement("canvas");
|
|
|
|
canvas.width = size[0];
|
|
|
|
canvas.height = size[1];
|
|
|
|
var ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
|
//draw chars
|
|
|
|
var widths = t.cwdh.info;
|
|
|
|
var position = 0;
|
|
|
|
|
|
|
|
for (var i=0; i<mapped.length; i++) {
|
|
|
|
var c = mapped[i];
|
|
|
|
var cinfo = widths[c];
|
|
|
|
|
|
|
|
var data = getCharData(c, palette);
|
|
|
|
ctx.putImageData(data, position + cinfo.pixelStart, 0, 0, 0, cinfo.pixelWidth, data.height);
|
|
|
|
|
|
|
|
position += cinfo.pixelLength + spacing;
|
|
|
|
}
|
|
|
|
return canvas;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCharData(id, pal) {
|
|
|
|
//todo: cache?
|
|
|
|
var cglp = t.cglp;
|
|
|
|
var tile = cglp.tiles[id];
|
|
|
|
var pixels = cglp.tileWidth*cglp.tileHeight;
|
|
|
|
var d = new Uint8ClampedArray(pixels*4);
|
|
|
|
var data = new ImageData(d, cglp.tileWidth, cglp.tileHeight);
|
|
|
|
var depth = t.cglp.depth;
|
|
|
|
var mask = (1<<depth)-1;
|
|
|
|
|
|
|
|
var bit = 8;
|
|
|
|
var byte = 0;
|
|
|
|
var curByte = tile[byte];
|
|
|
|
var ind = 0;
|
|
|
|
for (var i=0; i<pixels; i++) {
|
|
|
|
bit -= depth;
|
|
|
|
var pind = 0;
|
|
|
|
if (bit < 0) {
|
|
|
|
//overlap into next
|
|
|
|
var end = bit + 8;
|
|
|
|
if (end < 8) {
|
|
|
|
//still some left in this byte
|
|
|
|
pind = (curByte << (-bit)) & mask;
|
|
|
|
}
|
|
|
|
curByte = tile[++byte];
|
|
|
|
bit += 8;
|
|
|
|
pind |= (curByte >> (bit)) & mask;
|
|
|
|
} else {
|
|
|
|
pind = (curByte >> (bit)) & mask;
|
|
|
|
}
|
|
|
|
|
|
|
|
var col = pal[pind];
|
|
|
|
d[ind++] = col[0];
|
|
|
|
d[ind++] = col[1];
|
|
|
|
d[ind++] = col[2];
|
|
|
|
d[ind++] = col[3];
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2017-09-08 09:24:16 -07:00
|
|
|
}
|