#include "cell.h" #include "prosperon.h" #include "HandmadeMath.h" #include "stb_ds.h" #include "sdl.h" #include #include #include #include #include #include #include #include "stb_image_write.h" #include "stb_rect_pack.h" #define STB_TRUETYPE_IMPLEMENTATION #define STB_TRUETYPE_NO_STDIO #include "stb_truetype.h" #define STB_DS_IMPLEMENTATION #include "stb_ds.h" #include "HandmadeMath.h" typedef enum { LEFT, RIGHT, CENTER, JUSTIFY } TEXT_ALIGN; typedef enum { WORD, CHARACTER } TEXT_BREAK; struct text_char { rect pos; rect uv; HMM_Vec4 color; }; struct shader; struct window; struct character { float advance; rect quad; rect uv; float xoff; float yoff; float width; float height; }; // text data struct sFont { uint32_t height; /* in pixels */ float ascent; // pixels float descent; // pixels float linegap; //pixels float line_height; // pixels struct character Characters[256]; SDL_Surface *surface; }; typedef struct sFont font; typedef struct Character glyph; typedef struct { const char *start, *end; float width; } line_t; typedef struct { line_t *lines; float max_width; } layout_lines; struct sFont *use_font; void font_free(JSRuntime *rt, font *f) { if (f->surface) SDL_DestroySurface(f->surface); free(f); } struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) { if (!ttf_buffer) return NULL; int packsize = 1024; struct sFont *newfont = calloc(1, sizeof(struct sFont)); newfont->height = height; unsigned char *bitmap = malloc(packsize * packsize); stbtt_packedchar glyphs[95]; stbtt_pack_context pc; int pad = 2; stbtt_PackBegin(&pc, bitmap, packsize, packsize, 0, pad, NULL); stbtt_PackFontRange(&pc, ttf_buffer, 0, height, 32, 95, glyphs); stbtt_PackEnd(&pc); stbtt_fontinfo fontinfo; if (!stbtt_InitFont(&fontinfo, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer, 0))) { // YughError("Failed to make font %s", fontfile); } int ascent, descent, linegap; stbtt_GetFontVMetrics(&fontinfo, &ascent, &descent, &linegap); float s = stbtt_ScaleForPixelHeight(&fontinfo, height); newfont->ascent = ascent * s; newfont->descent = descent * s; /* descent is negative */ newfont->linegap = linegap * s; newfont->line_height = (newfont->ascent - newfont->descent) + newfont->linegap; newfont->surface = SDL_CreateSurface(packsize,packsize, SDL_PIXELFORMAT_RGBA32); if (!newfont->surface) printf("SDL ERROR: %s\n", SDL_GetError()); for (int i = 0; i < packsize; i++) for (int j = 0; j < packsize; j++) if (!SDL_WriteSurfacePixel(newfont->surface, j, i, 255,255,255,bitmap[i*packsize+j])) printf("SDLERROR: %s\n", SDL_GetError()); for (unsigned char c = 32; c < 127; c++) { stbtt_packedchar glyph = glyphs[c - 32]; rect uv; uv.x = (glyph.x0) / (float)packsize; uv.w = (glyph.x1-glyph.x0) / (float)packsize; uv.y = (glyph.y1) / (float)packsize; uv.h = (glyph.y0-glyph.y1) / (float)packsize; newfont->Characters[c].uv = uv; rect quad; quad.x = glyph.xoff; quad.y = -glyph.yoff2; // Top of glyph relative to baseline quad.w = glyph.xoff2 - glyph.xoff; quad.h = glyph.yoff2 - glyph.yoff; // Height from baseline newfont->Characters[c].quad = quad; newfont->Characters[c].advance = glyph.xadvance; } free(bitmap); return newfont; } layout_lines layout_text_lines(const char *text, font *f, float letter_spacing, float wrap, TEXT_BREAK break_at) { layout_lines out = {0}; int break_at_word = (break_at == WORD); const char *line_start = text; const char *word_start = text; float line_width = 0; float word_width = 0; float max_width = 0; for (const char *p = text; *p; p++) { if (*p == '\n') { line_t L = { line_start, p, line_width }; arrput(out.lines, L); if (line_width > max_width) max_width = line_width; line_start = p + 1; word_start = p + 1; line_width = 0; word_width = 0; continue; } unsigned char ch = (unsigned char)*p; float char_w = f->Characters[ch].advance + letter_spacing; if (wrap > 0 && line_width + char_w > wrap) { const char *break_pt = p; float break_w = line_width; if (break_at_word && *p != ' ' && word_width > 0 && word_start > line_start) { break_pt = word_start; break_w = line_width - word_width; } line_t L = { line_start, break_pt, break_w }; arrput(out.lines, L); if (break_w > max_width) max_width = break_w; line_start = break_pt; if (break_at_word && *line_start == ' ') line_start++; p = line_start - 1; word_start = line_start; line_width = 0; word_width = 0; continue; } line_width += char_w; if (break_at_word) { if (*p == ' ') { word_width = 0; word_start = p + 1; } else { word_width += char_w; } } } if (line_start < text + strlen(text)) { line_t L = { line_start, text + strlen(text), line_width }; arrput(out.lines, L); if (line_width > max_width) max_width = line_width; } out.max_width = max_width; return out; } void sdrawCharacter(struct text_vert **buffer, stbtt_packedchar c, HMM_Vec2 cursor, float scale, struct rgba color) { struct text_vert vert; // vert.pos.x = cursor.X + c.leftbearing; // vert.pos.y = cursor.Y + c.topbearing; // vert.wh = c.size; // if (vert.pos.x > frame.l || vert.pos.y > frame.t || (vert.pos.y + vert.wh.y) < frame.b || (vert.pos.x + vert.wh.x) < frame.l) return; // vert.uv.x = c.rect.x; // vert.uv.y = c.rect.y; // vert.st.x = c.rect.w; // vert.st.y = c.rect.h; rgba2floats(vert.color.e, color); arrput(*buffer, vert); } void draw_char_verts(struct text_vert **buffer, struct character c, HMM_Vec2 cursor, colorf color) { // packedchar has // Adds four verts: bottom left, bottom right, top left, top right text_vert bl; bl.pos.x = cursor.X + c.quad.x; bl.pos.y = cursor.Y + c.quad.y; bl.uv.x = c.uv.x; bl.uv.y = c.uv.y; bl.color = color; arrput(*buffer, bl); text_vert br = bl; br.pos.x += c.quad.w; br.uv.x += c.uv.w; arrput(*buffer, br); text_vert ul = bl; ul.pos.y += c.quad.h; ul.uv.y += c.uv.h; arrput(*buffer, ul); text_vert ur = ul; ur.pos.x = br.pos.x; ur.uv.x = br.uv.x; arrput(*buffer, ur); } const char *esc_color(const char *c, struct rgba *color, struct rgba defc) { struct rgba d; if (!color) color = &d; if (*c != '\033') return c; c++; if (*c != '[') return c; c++; if (*c == '0') { *color = defc; c++; return c; } else if (!strncmp(c, "38;2;", 5)) { c += 5; *color = (struct rgba){0,0,0,255}; color->r = atoi(c); c = strchr(c, ';')+1; color->g = atoi(c); c = strchr(c,';')+1; color->b = atoi(c); c = strchr(c,';')+1; return c; } return c; } // text is a string, font f, wrap is how long a line is before wrapping. -1 to not wrap HMM_Vec2 measure_text(const char *text, font *f, float letter_spacing, float wrap, TEXT_BREAK break_at, TEXT_ALIGN align) { layout_lines lay = layout_text_lines(text, f, letter_spacing, wrap, break_at); float lh = f->line_height; int line_count = arrlen(lay.lines); float height = line_count > 0 ? line_count * lh : 0; float width = lay.max_width; if (wrap > 0 && align != LEFT && align != JUSTIFY) { // For RIGHT or CENTER we might shift lines visually, but width is still max(line.width, wrap ? min(wrap, ...) ). // Usually you'd report max(wrap, width) or just wrap, depending on how you want containers to size. } HMM_Vec2 dim = { width, height }; arrfree(lay.lines); return dim; } /* pos given in screen coordinates */ struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf color, float wrap, TEXT_BREAK break_at, TEXT_ALIGN align, float letter_spacing) { text_vert *buffer = NULL; float lh = f->line_height; layout_lines lay = layout_text_lines(text, f, letter_spacing, wrap, break_at); line_t *lines = lay.lines; HMM_Vec2 cursor = pos; for (int i = 0; i < arrlen(lines); i++) { line_t *L = &lines[i]; float start_x = pos.x; float extra_space = 0; int space_count = 0; if (wrap > 0) { switch (align) { case RIGHT: start_x = pos.x + wrap - L->width; break; case CENTER: start_x = pos.x + (wrap - L->width) * 0.5f; break; case JUSTIFY: if (i < arrlen(lines) - 1 && *(L->end) != '\n') { for (const char *p = L->start; p < L->end; p++) if (*p == ' ') space_count++; if (space_count > 0) extra_space = (wrap - L->width) / space_count; } break; case LEFT: default: break; } } cursor.x = start_x; const char *p = L->start; while (p < L->end) { unsigned char ch = (unsigned char)*p; struct character g = f->Characters[ch]; if (!isspace(*p)) draw_char_verts(&buffer, g, cursor, color); cursor.x += g.advance; /* add letter spacing unless this is last char of the line */ if (p + 1 < L->end) cursor.x += letter_spacing; if (align == JUSTIFY && *p == ' ') cursor.x += extra_space; p++; } cursor.x = pos.x; cursor.y -= lh; } arrfree(lines); return buffer; } // QuickJS class for font QJSCLASS(font,) // Font constructor JSC_CCALL(staef_font_new, size_t len; void *data = js_get_blob_data(js, &len, argv[0]); if (data == -1) return JS_EXCEPTION; if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data"); double height = js2number(js, argv[1]); font *f = MakeFont(data, len, (int)height); if (!f) return JS_ThrowReferenceError(js, "could not create font"); ret = font2js(js, f); // Create surface data object for the font's atlas if (f->surface) { JSValue surfData = JS_NewObject(js); JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, f->surface->w)); JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, f->surface->h)); JS_SetPropertyStr(js, surfData, "format", pixelformat2js(js, f->surface->format)); JS_SetPropertyStr(js, surfData, "pitch", JS_NewInt32(js, f->surface->pitch)); // Lock surface if needed int locked = 0; if (SDL_MUSTLOCK(f->surface)) { if (SDL_LockSurface(f->surface) < 0) return JS_ThrowInternalError(js, "Lock surface failed: %s", SDL_GetError()); locked = 1; } size_t byte_size = f->surface->pitch * f->surface->h; JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, f->surface->pixels, byte_size)); if (locked) SDL_UnlockSurface(f->surface); JS_SetPropertyStr(js, ret, "surface", surfData); } ) // Calculate text size JSC_CCALL(staef_font_text_size, font *f = js2font(js, self); const char *str = JS_ToCString(js, argv[0]); float letterSpacing = argc > 1 ? js2number(js, argv[1]) : 0; float wrap = argc > 2 ? js2number(js, argv[2]) : -1; const char *breakat = argc > 3 ? JS_ToCString(js, argv[3]) : NULL; int breakAt = (breakat && strcmp(breakat, "character") == 0) ? CHARACTER : WORD; const char *align = argc > 4 ? JS_ToCString(js, argv[4]) : NULL; int alignAt = (align && strcmp(align, "right") == 0) ? RIGHT : (align && strcmp(align, "center") == 0) ? CENTER : (align && strcmp(align, "justify") == 0) ? JUSTIFY : LEFT; ret = vec22js(js, measure_text(str, f, letterSpacing, wrap, breakAt, alignAt)); JS_FreeCString(js, str); if (breakat) JS_FreeCString(js, breakat); if (align) JS_FreeCString(js, align); ) // Generate text buffer (mesh data) JSC_CCALL(staef_font_make_text_buffer, font *f = js2font(js, self); const char *s = JS_ToCString(js, argv[0]); rect rectpos = js2rect(js, argv[1]); colorf c = js2color(js, argv[2]); float wrap = argc > 3 ? js2number(js, argv[3]) : -1; const char *breakat = argc > 4 ? JS_ToCString(js, argv[4]) : NULL; int breakAt = (breakat && strcmp(breakat, "character") == 0) ? CHARACTER : WORD; const char *align = argc > 5 ? JS_ToCString(js, argv[5]) : NULL; int alignAt = (align && strcmp(align, "right") == 0) ? RIGHT : (align && strcmp(align, "center") == 0) ? CENTER : (align && strcmp(align, "justify") == 0) ? JUSTIFY : LEFT; HMM_Vec2 startpos = {.x = rectpos.x, .y = rectpos.y }; text_vert *buffer = renderText(s, startpos, f, c, wrap, breakAt, alignAt, 0); ret = quads_to_mesh(js, buffer); JS_FreeCString(js, s); if (breakat) JS_FreeCString(js, breakat); if (align) JS_FreeCString(js, align); arrfree(buffer); ) // Font property getters/setters JSC_GETSET(font, linegap, number) JSC_GETSET(font, ascent, number) JSC_GETSET(font, descent, number) JSC_GETSET(font, line_height, number) JSC_GETSET(font, height, number) // Font methods static const JSCFunctionListEntry js_font_funcs[] = { MIST_FUNC_DEF(staef_font, text_size, 5), MIST_FUNC_DEF(staef_font, make_text_buffer, 6), CGETSET_ADD(font, linegap), CGETSET_ADD(font, ascent), CGETSET_ADD(font, descent), CGETSET_ADD(font, line_height), CGETSET_ADD(font, height), }; // Font constructor function static JSValue js_font_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { return js_staef_font_new(ctx, JS_NULL, argc, argv); } // Initialize the staef module CELL_USE_INIT( JSValue mod = JS_NewObject(js); JSValue proto; // Initialize font class JS_NewClassID(&js_font_id); JS_NewClass(JS_GetRuntime(js), js_font_id, &js_font_class); proto = JS_NewObject(js); JS_SetPropertyFunctionList(js, proto, js_font_funcs, countof(js_font_funcs)); JS_SetClassProto(js, js_font_id, proto); // Create font constructor JSValue font_ctor = JS_NewCFunction2(js, js_font_constructor, "font", 2, JS_CFUNC_constructor, 0); JS_SetConstructor(js, font_ctor, proto); // Add font constructor to module (lowercase to match "new staef.font") JS_SetPropertyStr(js, mod, "font", font_ctor); return mod; )