222 lines
4.8 KiB
Plaintext
222 lines
4.8 KiB
Plaintext
module ugui;
|
|
|
|
import schrift;
|
|
import std::collections::map;
|
|
import std::core::mem;
|
|
import std::io;
|
|
|
|
// unicode code point, different type for a different hash
|
|
def Codepoint = uint;
|
|
|
|
|
|
/* width and height of a glyph contain the kering advance
|
|
* (u,v)
|
|
* +-------------*---+ -
|
|
* | ^ | | ^
|
|
* | |oy | | |
|
|
* | v | | |
|
|
* | .ii. | | |
|
|
* | @@@@@@. | | |
|
|
* | V@Mio@@o | | |
|
|
* | :i. V@V | | h
|
|
* | :oM@@M | | |
|
|
* | :@@@MM@M | | |
|
|
* | @@o o@M | | |
|
|
* |<->:@@. M@M | | |
|
|
* |ox @@@o@@@@ | | |
|
|
* | :M@@V:@@.| | v
|
|
* +-------------*---+ -
|
|
* |<---- w ---->|
|
|
* |<------ adv ---->|
|
|
*/
|
|
struct Glyph {
|
|
Codepoint code;
|
|
ushort u, v;
|
|
ushort w, h;
|
|
short adv, ox, oy;
|
|
short idx; // atlas index
|
|
}
|
|
|
|
const uint FONT_CACHED = 512;
|
|
def GlyphTable = map::HashMap(<Codepoint, Glyph>) @private;
|
|
|
|
fault UgFontError {
|
|
TTF_LOAD_FAILED,
|
|
MISSING_GLYPH,
|
|
BAD_GLYPH_METRICS,
|
|
RENDER_ERROR,
|
|
}
|
|
|
|
fault UgAtlasError {
|
|
CANNOT_PLACE,
|
|
}
|
|
|
|
// black and white atlas
|
|
struct AtlasBW {
|
|
ushort width, height;
|
|
char[] buffer;
|
|
|
|
Point row;
|
|
ushort row_h;
|
|
}
|
|
|
|
struct Font {
|
|
schrift::Sft sft;
|
|
String path;
|
|
GlyphTable table;
|
|
|
|
float size;
|
|
float ascender, descender, linegap; // Line Metrics
|
|
AtlasBW[] atlas;
|
|
}
|
|
|
|
fn void! AtlasBW.new(&atlas, ushort width, ushort height)
|
|
{
|
|
atlas.width = width;
|
|
atlas.height = height;
|
|
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height);
|
|
}
|
|
|
|
fn void AtlasBW.free(&atlas)
|
|
{
|
|
free(atlas.buffer);
|
|
}
|
|
|
|
// place a rect inside the atlas
|
|
// uses a row first algorithm
|
|
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
|
|
fn Point! AtlasBW.place(&atlas, char[] pixels, ushort w, ushort h)
|
|
{
|
|
Point p;
|
|
|
|
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
|
p = atlas.row;
|
|
} else {
|
|
atlas.row.x = 0;
|
|
atlas.row.y = atlas.row.y + atlas.row_h;
|
|
atlas.row_h = 0;
|
|
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
|
p = atlas.row;
|
|
} else {
|
|
return UgAtlasError.CANNOT_PLACE?;
|
|
}
|
|
}
|
|
|
|
for (usz y = 0; y < h; y++) {
|
|
for (usz x = 0; x < w; x++) {
|
|
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
|
|
}
|
|
}
|
|
|
|
atlas.row.x += w;
|
|
if (h > atlas.row_h) {
|
|
atlas.row_h = h;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
fn void! Font.load(&font, String path, uint height, float scale = 1)
|
|
{
|
|
font.table.new_init(capacity: FONT_CACHED);
|
|
|
|
font.size = height*scale;
|
|
|
|
font.sft = schrift::Sft{
|
|
.xScale = (double)font.size,
|
|
.yScale = (double)font.size,
|
|
.flags = schrift::SFT_DOWNWARD_Y,
|
|
};
|
|
|
|
font.sft.font = schrift::loadfile(path);
|
|
if (font.sft.font == null) {
|
|
return UgFontError.TTF_LOAD_FAILED?;
|
|
}
|
|
|
|
schrift::SftLMetrics lmetrics;
|
|
schrift::lmetrics(&font.sft, &lmetrics);
|
|
font.ascender = (float)lmetrics.ascender;
|
|
font.descender = (float)lmetrics.descender;
|
|
font.linegap = (float)lmetrics.lineGap;
|
|
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
|
|
|
|
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
|
|
// like the letter 'A'
|
|
font.atlas = mem::new_array(AtlasBW, 1);
|
|
ushort size = (ushort)font.size*256;
|
|
font.atlas[0].new(size, size)!;
|
|
|
|
// preallocate the ASCII range
|
|
// for (char c = ' '; c < '~'; c++) {
|
|
// font.get_glyph((Codepoint)c)!;
|
|
// }
|
|
}
|
|
|
|
fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
|
|
{
|
|
Glyph*! gp;
|
|
gp = font.table.get_ref(code);
|
|
|
|
if (catch excuse = gp) {
|
|
if (excuse != SearchResult.MISSING) {
|
|
return excuse?;
|
|
}
|
|
} else {
|
|
if (is_new) { *is_new = false; }
|
|
return gp;
|
|
}
|
|
|
|
// missing glyph, render and place into an atlas
|
|
Glyph glyph;
|
|
|
|
schrift::SftGlyph gid;
|
|
schrift::SftGMetrics gmtx;
|
|
|
|
if (schrift::lookup(&font.sft, code, &gid) < 0) {
|
|
return UgFontError.MISSING_GLYPH?;
|
|
}
|
|
|
|
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
|
|
return UgFontError.BAD_GLYPH_METRICS?;
|
|
}
|
|
|
|
schrift::SftImage img = {
|
|
.width = gmtx.minWidth,
|
|
.height = gmtx.minHeight,
|
|
};
|
|
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
|
|
img.pixels = pixels;
|
|
if (schrift::render(&font.sft, gid, img) < 0) {
|
|
return UgFontError.RENDER_ERROR?;
|
|
}
|
|
|
|
glyph.code = code;
|
|
glyph.w = (ushort)img.width;
|
|
glyph.h = (ushort)img.height;
|
|
glyph.ox = (short)gmtx.leftSideBearing;
|
|
glyph.oy = (short)gmtx.yOffset;
|
|
glyph.adv = (short)gmtx.advanceWidth;
|
|
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
|
|
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
|
|
|
|
Point uv = font.atlas[0].place(pixels, glyph.w, glyph.h)!;
|
|
glyph.idx = 0;
|
|
glyph.u = uv.x;
|
|
glyph.v = uv.y;
|
|
|
|
mem::free(pixels);
|
|
|
|
font.table.set(code, glyph);
|
|
|
|
if (is_new) { *is_new = true; }
|
|
return font.table.get_ref(code);
|
|
}
|
|
|
|
fn void Font.free(&font)
|
|
{
|
|
foreach (atlas: font.atlas) {
|
|
atlas.free();
|
|
}
|
|
schrift::freefont(font.sft.font);
|
|
}
|