#include "Drawer2D.h" #include "String.h" #include "Graphics.h" #include "Funcs.h" #include "Platform.h" #include "ExtMath.h" #include "Logger.h" #include "Game.h" #include "Event.h" #include "Chat.h" #include "Stream.h" #include "Utils.h" #include "Errors.h" #include "Window.h" #include "Options.h" #include "TexturePack.h" #include "SystemFonts.h" struct _Drawer2DData Drawer2D; #define Font_IsBitmap(font) (!(font)->handle) void DrawTextArgs_Make(struct DrawTextArgs* args, STRING_REF const cc_string* text, struct FontDesc* font, cc_bool useShadow) { args->text = *text; args->font = font; args->useShadow = useShadow; } void DrawTextArgs_MakeEmpty(struct DrawTextArgs* args, struct FontDesc* font, cc_bool useShadow) { args->text = String_Empty; args->font = font; args->useShadow = useShadow; } /*########################################################################################################################* *-----------------------------------------------------Font functions------------------------------------------------------* *#########################################################################################################################*/ int Drawer2D_AdjHeight(int point) { return Math_CeilDiv(point * 3, 2); } void Font_MakeBitmapped(struct FontDesc* desc, int size, int flags) { /* TODO: Scale X and Y independently */ size = Display_ScaleY(size); desc->handle = NULL; desc->size = size; desc->flags = flags; desc->height = Drawer2D_AdjHeight(size); } void Font_Make(struct FontDesc* desc, int size, int flags) { if (Drawer2D.BitmappedText) { Font_MakeBitmapped(desc, size, flags); } else { SysFont_MakeDefault(desc, size, flags); } } void Font_Free(struct FontDesc* desc) { desc->size = 0; if (Font_IsBitmap(desc)) return; SysFont_Free(desc); desc->handle = NULL; } static struct Bitmap fontBitmap; static int tileSize = 8; /* avoid divide by 0 if default.png missing */ /* So really 16 characters per row */ #define LOG2_CHARS_PER_ROW 4 static int tileWidths[256]; /* Finds the right-most non-transparent pixel in each tile in default.png */ static void CalculateTextWidths(void) { int width = fontBitmap.width, height = fontBitmap.height; BitmapCol* row; int i, x, y, xx, tileY; for (y = 0; y < height; y++) { tileY = y / tileSize; row = Bitmap_GetRow(&fontBitmap, y); i = 0 | (tileY << LOG2_CHARS_PER_ROW); /* Iterate through each tile on current scanline */ for (x = 0; x < width; x += tileSize, i++) { /* Iterate through each pixel of the given character, on the current scanline */ for (xx = tileSize - 1; xx >= 0; xx--) { if (!BitmapCol_A(row[x + xx])) continue; /* Check if this is the pixel furthest to the right, for the current character */ tileWidths[i] = max(tileWidths[i], xx + 1); break; } } } tileWidths[' '] = tileSize / 4; } static void FreeFontBitmap(void) { int i; for (i = 0; i < Array_Elems(tileWidths); i++) tileWidths[i] = 0; Mem_Free(fontBitmap.scan0); } cc_bool Font_SetBitmapAtlas(struct Bitmap* bmp) { /* If not all of these cases are accounted for, end up overwriting memory after tileWidths */ if (bmp->width != bmp->height) { static const cc_string msg = String_FromConst("&cWidth of default.png must equal its height"); Logger_WarnFunc(&msg); return false; } else if (bmp->width < 16) { static const cc_string msg = String_FromConst("&cdefault.png must be at least 16 pixels wide"); Logger_WarnFunc(&msg); return false; } else if (!Math_IsPowOf2(bmp->width)) { static const cc_string msg = String_FromConst("&cWidth of default.png must be a power of two"); Logger_WarnFunc(&msg); return false; } /* TODO: Use shift instead of mul/div */ FreeFontBitmap(); fontBitmap = *bmp; tileSize = bmp->width >> LOG2_CHARS_PER_ROW; CalculateTextWidths(); return true; } void Font_SetPadding(struct FontDesc* desc, int amount) { if (!Font_IsBitmap(desc)) return; desc->height = desc->size + Display_ScaleY(amount) * 2; } /*########################################################################################################################* *---------------------------------------------------Drawing functions-----------------------------------------------------* *#########################################################################################################################*/ cc_bool Drawer2D_Clamp(struct Context2D* ctx, int* x, int* y, int* width, int* height) { if (*x >= ctx->width || *y >= ctx->height) return false; /* origin is negative, move inside */ if (*x < 0) { *width += *x; *x = 0; } if (*y < 0) { *height += *y; *y = 0; } *width = min(*x + *width, ctx->width) - *x; *height = min(*y + *height, ctx->height) - *y; return *width > 0 && *height > 0; } #define Drawer2D_ClampPixel(p) p = (p < 0 ? 0 : (p > 255 ? 255 : p)) void Context2D_Alloc(struct Context2D* ctx, int width, int height) { ctx->width = width; ctx->height = height; ctx->meta = NULL; if (!Gfx.SupportsNonPowTwoTextures) { /* Allocate power-of-2 sized bitmap equal to or greater than the given size */ width = Math_NextPowOf2(width); height = Math_NextPowOf2(height); } if (Gfx.MinTexWidth) { width = max(width, Gfx.MinTexWidth); } if (Gfx.MinTexHeight) { height = max(height, Gfx.MinTexHeight); } ctx->bmp.width = width; ctx->bmp.height = height; ctx->bmp.scan0 = (BitmapCol*)Mem_AllocCleared(width * height, 4, "bitmap data"); } void Context2D_Wrap(struct Context2D* ctx, struct Bitmap* bmp) { ctx->bmp = *bmp; ctx->width = bmp->width; ctx->height = bmp->height; ctx->meta = NULL; } void Context2D_Free(struct Context2D* ctx) { Mem_Free(ctx->bmp.scan0); } #define BitmapColor_Raw(r, g, b) (BitmapColor_R_Bits(r) | BitmapColor_G_Bits(g) | BitmapColor_B_Bits(b)) void Gradient_Noise(struct Context2D* ctx, BitmapCol color, int variation, int x, int y, int width, int height) { struct Bitmap* bmp = (struct Bitmap*)ctx; BitmapCol* dst; int R, G, B, xx, yy, n; int noise, delta; cc_uint32 alpha; if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; alpha = color & BITMAPCOLOR_A_MASK; for (yy = 0; yy < height; yy++) { dst = Bitmap_GetRow(bmp, y + yy) + x; for (xx = 0; xx < width; xx++, dst++) { n = (x + xx) + (y + yy) * 57; n = (n << 13) ^ n; /* float noise = 1.0f - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0f; int delta = (int)(noise * variation); */ /* Fixed point equivalent to the above expression */ noise = ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff); delta = (((1024 - noise / 0x100000)) * variation) >> 10; R = BitmapCol_R(color) + delta; Drawer2D_ClampPixel(R); G = BitmapCol_G(color) + delta; Drawer2D_ClampPixel(G); B = BitmapCol_B(color) + delta; Drawer2D_ClampPixel(B); *dst = BitmapColor_Raw(R, G, B) | alpha; } } } void Gradient_Vertical(struct Context2D* ctx, BitmapCol a, BitmapCol b, int x, int y, int width, int height) { struct Bitmap* bmp = (struct Bitmap*)ctx; BitmapCol* row, color; int xx, yy; float t; if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; for (yy = 0; yy < height; yy++) { row = Bitmap_GetRow(bmp, y + yy) + x; t = (float)yy / (height - 1); /* so last row has color of b */ color = BitmapCol_Make( Math_Lerp(BitmapCol_R(a), BitmapCol_R(b), t), Math_Lerp(BitmapCol_G(a), BitmapCol_G(b), t), Math_Lerp(BitmapCol_B(a), BitmapCol_B(b), t), 255); for (xx = 0; xx < width; xx++) { row[xx] = color; } } } void Gradient_Blend(struct Context2D* ctx, BitmapCol color, int blend, int x, int y, int width, int height) { struct Bitmap* bmp = (struct Bitmap*)ctx; BitmapCol* dst; int R, G, B, xx, yy; if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; /* Pre compute the alpha blended source color */ /* TODO: Avoid shift when multiplying */ color = BitmapCol_Make( BitmapCol_R(color) * blend / 255, BitmapCol_G(color) * blend / 255, BitmapCol_B(color) * blend / 255, 0); blend = 255 - blend; /* inverse for existing pixels */ for (yy = 0; yy < height; yy++) { dst = Bitmap_GetRow(bmp, y + yy) + x; for (xx = 0; xx < width; xx++, dst++) { /* TODO: Not shift when multiplying */ R = BitmapCol_R(color) + (BitmapCol_R(*dst) * blend) / 255; G = BitmapCol_G(color) + (BitmapCol_G(*dst) * blend) / 255; B = BitmapCol_B(color) + (BitmapCol_B(*dst) * blend) / 255; *dst = BitmapColor_RGB(R, G, B); } } } void Context2D_DrawPixels(struct Context2D* ctx, int x, int y, struct Bitmap* src) { struct Bitmap* dst = (struct Bitmap*)ctx; int width = src->width, height = src->height; BitmapCol* dstRow; BitmapCol* srcRow; int xx, yy; if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; for (yy = 0; yy < height; yy++) { srcRow = Bitmap_GetRow(src, yy); dstRow = Bitmap_GetRow(dst, y + yy) + x; for (xx = 0; xx < width; xx++) { dstRow[xx] = srcRow[xx]; } } } void Context2D_Clear(struct Context2D* ctx, BitmapCol color, int x, int y, int width, int height) { struct Bitmap* bmp = (struct Bitmap*)ctx; BitmapCol* row; int xx, yy; if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; for (yy = 0; yy < height; yy++) { row = Bitmap_GetRow(bmp, y + yy) + x; for (xx = 0; xx < width; xx++) { row[xx] = color; } } } void Drawer2D_MakeTextTexture(struct Texture* tex, struct DrawTextArgs* args) { static struct Texture empty = { 0, Tex_Rect(0,0, 0,0), Tex_UV(0,0, 1,1) }; struct Context2D ctx; int width, height; /* pointless to draw anything when context is lost */ if (Gfx.LostContext) { *tex = empty; return; } width = Drawer2D_TextWidth(args); if (!width) { *tex = empty; return; } height = Drawer2D_TextHeight(args); Context2D_Alloc(&ctx, width, height); { Context2D_DrawText(&ctx, args, 0, 0); Context2D_MakeTexture(tex, &ctx); } Context2D_Free(&ctx); } void Context2D_MakeTexture(struct Texture* tex, struct Context2D* ctx) { int flags = TEXTURE_FLAG_NONPOW2 | TEXTURE_FLAG_LOWRES; Gfx_RecreateTexture(&tex->ID, &ctx->bmp, flags, false); tex->width = ctx->width; tex->height = ctx->height; tex->uv.u1 = 0.0f; tex->uv.v1 = 0.0f; tex->uv.u2 = (float)ctx->width / (float)ctx->bmp.width; tex->uv.v2 = (float)ctx->height / (float)ctx->bmp.height; } cc_bool Drawer2D_ValidColorCodeAt(const cc_string* text, int i) { if (i >= text->length) return false; return BitmapCol_A(Drawer2D_GetColor(text->buffer[i])) != 0; } cc_bool Drawer2D_UNSAFE_NextPart(cc_string* left, cc_string* part, char* colorCode) { BitmapCol color; char cur; int i; /* check if current part starts with a colour code */ if (left->length >= 2 && left->buffer[0] == '&') { cur = left->buffer[1]; color = Drawer2D_GetColor(cur); if (BitmapCol_A(color)) { *colorCode = cur; left->buffer += 2; left->length -= 2; } } for (i = 0; i < left->length; i++) { if (left->buffer[i] == '&' && Drawer2D_ValidColorCodeAt(left, i + 1)) break; } /* advance string starts and lengths */ part->buffer = left->buffer; part->length = i; left->buffer += i; left->length -= i; return part->length > 0 || left->length > 0; } cc_bool Drawer2D_IsEmptyText(const cc_string* text) { cc_string left = *text, part; char colorCode; while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode)) { if (part.length) return false; } return true; } void Drawer2D_WithoutColors(cc_string* str, const cc_string* src) { cc_string left = *src, part; char colorCode; while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode)) { String_AppendString(str, &part); } } char Drawer2D_LastColor(const cc_string* text, int start) { int i; if (start >= text->length) start = text->length - 1; for (i = start; i >= 0; i--) { if (text->buffer[i] != '&') continue; if (Drawer2D_ValidColorCodeAt(text, i + 1)) { return text->buffer[i + 1]; } } return '\0'; } cc_bool Drawer2D_IsWhiteColor(char c) { return c == '\0' || c == 'f' || c == 'F'; } /* TODO: Needs to account for DPI */ #define Drawer2D_ShadowOffset(point) (point / 8) #define Drawer2D_XPadding(point) (Math_CeilDiv(point, 8)) static int Drawer2D_Width(int point, char c) { return Math_CeilDiv(tileWidths[(cc_uint8)c] * point, tileSize); } void Drawer2D_ReducePadding_Tex(struct Texture* tex, int point, int scale) { int padding; float vAdj; if (!Drawer2D.BitmappedText) return; padding = (tex->height - point) / scale; vAdj = (float)padding / Math_NextPowOf2(tex->height); tex->uv.v1 += vAdj; tex->uv.v2 -= vAdj; tex->height -= (cc_uint16)(padding * 2); } void Drawer2D_ReducePadding_Height(int* height, int point, int scale) { int padding; if (!Drawer2D.BitmappedText) return; padding = (*height - point) / scale; *height -= padding * 2; } void Drawer2D_Fill(struct Bitmap* bmp, int x, int y, int width, int height, BitmapCol color) { BitmapCol* row; int xx, yy; for (yy = y; yy < y + height; yy++) { if (yy >= bmp->height) return; row = Bitmap_GetRow(bmp, yy); for (xx = x; xx < x + width; xx++) { if (xx >= bmp->width) break; row[xx] = color; } } } static void DrawBitmappedTextCore(struct Bitmap* bmp, struct DrawTextArgs* args, int x, int y, cc_bool shadow) { BitmapCol color; cc_string text = args->text; int i, point = args->font->size, count = 0; int xPadding; int srcX, srcY, dstX, dstY; int fontX, fontY; int srcWidth, dstWidth; int dstHeight, begX, xx, yy; int cellY, underlineY, underlineHeight; BitmapCol* srcRow, src; BitmapCol* dstRow; cc_uint8 coords[DRAWER2D_MAX_TEXT_LENGTH]; BitmapCol colors[DRAWER2D_MAX_TEXT_LENGTH]; cc_uint16 dstWidths[DRAWER2D_MAX_TEXT_LENGTH]; color = Drawer2D.Colors['f']; if (shadow) color = GetShadowColor(color); for (i = 0; i < text.length; i++) { char c = text.buffer[i]; if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) { color = Drawer2D_GetColor(text.buffer[i + 1]); if (shadow) color = GetShadowColor(color); i++; continue; /* skip over the color code */ } coords[count] = c; colors[count] = color; dstWidths[count] = Drawer2D_Width(point, c); count++; } dstHeight = point; begX = x; /* adjust coords to make drawn text match GDI fonts */ y += (args->font->height - dstHeight) / 2; xPadding = Drawer2D_XPadding(point); for (yy = 0; yy < dstHeight; yy++) { dstY = y + yy; if ((unsigned)dstY >= (unsigned)bmp->height) continue; fontY = 0 + yy * tileSize / dstHeight; dstRow = Bitmap_GetRow(bmp, dstY); for (i = 0; i < count; i++) { srcX = (coords[i] & 0x0F) * tileSize; srcY = (coords[i] >> 4) * tileSize; srcRow = Bitmap_GetRow(&fontBitmap, fontY + srcY); srcWidth = tileWidths[coords[i]]; dstWidth = dstWidths[i]; color = colors[i]; for (xx = 0; xx < dstWidth; xx++) { fontX = srcX + xx * srcWidth / dstWidth; src = srcRow[fontX]; if (!BitmapCol_A(src)) continue; dstX = x + xx; if ((unsigned)dstX >= (unsigned)bmp->width) continue; /* TODO: Transparent text by multiplying by col.A */ /* TODO: Not shift when multiplying */ /* TODO: avoid BitmapCol_A shift */ dstRow[dstX] = BitmapCol_Make( BitmapCol_R(src) * BitmapCol_R(color) / 255, BitmapCol_G(src) * BitmapCol_G(color) / 255, BitmapCol_B(src) * BitmapCol_B(color) / 255, BitmapCol_A(src)); } x += dstWidth + xPadding; } x = begX; } if (!(args->font->flags & FONT_FLAGS_UNDERLINE)) return; /* scale up bottom row of a cell to drawn text font */ cellY = (8 - 1) * dstHeight / 8; underlineY = y + cellY; underlineHeight = dstHeight - cellY; for (i = 0; i < count; ) { dstWidth = 0; color = colors[i]; for (; i < count && color == colors[i]; i++) { dstWidth += dstWidths[i] + xPadding; } Drawer2D_Fill(bmp, x, underlineY, dstWidth, underlineHeight, color); x += dstWidth; } } static void DrawBitmappedText(struct Bitmap* bmp, struct DrawTextArgs* args, int x, int y) { int offset = Drawer2D_ShadowOffset(args->font->size); if (!fontBitmap.scan0) { if (args->useShadow) FallbackFont_DrawText(args, bmp, x, y, true); FallbackFont_DrawText(args, bmp, x, y, false); return; } if (args->useShadow) { DrawBitmappedTextCore(bmp, args, x + offset, y + offset, true); } DrawBitmappedTextCore(bmp, args, x, y, false); } static int MeasureBitmappedWidth(const struct DrawTextArgs* args) { int i, point = args->font->size; int xPadding, width; cc_string text; if (!fontBitmap.scan0) return FallbackFont_TextWidth(args); /* adjust coords to make drawn text match GDI fonts */ xPadding = Drawer2D_XPadding(point); width = 0; text = args->text; for (i = 0; i < text.length; i++) { char c = text.buffer[i]; if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) { i++; continue; /* skip over the color code */ } width += Drawer2D_Width(point, c) + xPadding; } if (!width) return 0; /* Remove padding at end */ if (!(args->font->flags & FONT_FLAGS_PADDING)) width -= xPadding; if (args->useShadow) { width += Drawer2D_ShadowOffset(point); } return width; } void Context2D_DrawText(struct Context2D* ctx, struct DrawTextArgs* args, int x, int y) { struct Bitmap* bmp = (struct Bitmap*)ctx; if (Drawer2D_IsEmptyText(&args->text)) return; if (Font_IsBitmap(args->font)) { DrawBitmappedText(bmp, args, x, y); return; } if (args->useShadow) { SysFont_DrawText(args, bmp, x, y, true); } SysFont_DrawText(args, bmp, x, y, false); } int Drawer2D_TextWidth(struct DrawTextArgs* args) { if (Font_IsBitmap(args->font)) return MeasureBitmappedWidth(args); return SysFont_TextWidth(args); } int Drawer2D_TextHeight(struct DrawTextArgs* args) { return Font_CalcHeight(args->font, args->useShadow); } int Font_CalcHeight(const struct FontDesc* font, cc_bool useShadow) { int height = font->height; if (Font_IsBitmap(font)) { if (useShadow) { height += Drawer2D_ShadowOffset(font->size); } } else { if (useShadow) height += 2; } return height; } void Drawer2D_DrawClippedText(struct Context2D* ctx, struct DrawTextArgs* args, int x, int y, int maxWidth) { char strBuffer[512]; struct DrawTextArgs part; int i, width; width = Drawer2D_TextWidth(args); /* No clipping needed */ if (width <= maxWidth) { Context2D_DrawText(ctx, args, x, y); return; } part = *args; String_InitArray(part.text, strBuffer); String_Copy(&part.text, &args->text); String_Append(&part.text, '.'); for (i = part.text.length - 2; i > 0; i--) { part.text.buffer[i] = '.'; /* skip over trailing spaces */ if (part.text.buffer[i - 1] == ' ') continue; part.text.length = i + 2; width = Drawer2D_TextWidth(&part); if (width <= maxWidth) { Context2D_DrawText(ctx, &part, x, y); return; } /* If down to <= 2 chars, try omitting the .. */ if (i > 2) continue; part.text.length = i; width = Drawer2D_TextWidth(&part); if (width <= maxWidth) { Context2D_DrawText(ctx, &part, x, y); return; } } } /*########################################################################################################################* *---------------------------------------------------Drawer2D component----------------------------------------------------* *#########################################################################################################################*/ static void DefaultPngProcess(struct Stream* stream, const cc_string* name) { struct Bitmap bmp; cc_result res; if ((res = Png_Decode(&bmp, stream))) { Logger_SysWarn2(res, "decoding", name); Mem_Free(bmp.scan0); } else if (Font_SetBitmapAtlas(&bmp)) { Event_RaiseVoid(&ChatEvents.FontChanged); } else { Mem_Free(bmp.scan0); } } static struct TextureEntry default_entry = { "default.png", DefaultPngProcess }; /* The default 16 colours are the CGA 16 color palette (without special brown colour) */ /* See https://en.wikipedia.org/wiki/Color_Graphics_Adapter#With_an_RGBI_monitor for reference */ /* The 16 hex colours below were produced from the following formula: */ /* R = 191 * ((hex >> 2) & 1) + 64 * (hex >> 3) */ /* G = 191 * ((hex >> 1) & 1) + 64 * (hex >> 3) */ /* B = 191 * ((hex >> 0) & 1) + 64 * (hex >> 3) */ static const BitmapCol defaults_0_9[] = { BitmapColor_RGB( 0, 0, 0), /* 0 */ BitmapColor_RGB( 0, 0, 191), /* 1 */ BitmapColor_RGB( 0, 191, 0), /* 2 */ BitmapColor_RGB( 0, 191, 191), /* 3 */ BitmapColor_RGB(191, 0, 0), /* 4 */ BitmapColor_RGB(191, 0, 191), /* 5 */ BitmapColor_RGB(191, 191, 0), /* 6 */ BitmapColor_RGB(191, 191, 191), /* 7 */ BitmapColor_RGB( 64, 64, 64), /* 8 */ BitmapColor_RGB( 64, 64, 255) /* 9 */ }; static const BitmapCol defaults_a_f[] = { BitmapColor_RGB( 64, 255, 64), /* A */ BitmapColor_RGB( 64, 255, 255), /* B */ BitmapColor_RGB(255, 64, 64), /* C */ BitmapColor_RGB(255, 64, 255), /* D */ BitmapColor_RGB(255, 255, 64), /* E */ BitmapColor_RGB(255, 255, 255), /* F */ }; static void OnReset(void) { Mem_Set(Drawer2D.Colors, 0, sizeof(Drawer2D.Colors)); Mem_Copy(&Drawer2D.Colors['0'], defaults_0_9, sizeof(defaults_0_9)); Mem_Copy(&Drawer2D.Colors['a'], defaults_a_f, sizeof(defaults_a_f)); Mem_Copy(&Drawer2D.Colors['A'], defaults_a_f, sizeof(defaults_a_f)); } static void OnInit(void) { OnReset(); TextureEntry_Register(&default_entry); Drawer2D.BitmappedText = Game_ClassicMode || !Options_GetBool(OPT_USE_CHAT_FONT, false); Drawer2D.BlackTextShadows = Options_GetBool(OPT_BLACK_TEXT, false); } static void OnFree(void) { FreeFontBitmap(); fontBitmap.scan0 = NULL; } struct IGameComponent Drawer2D_Component = { OnInit, /* Init */ OnFree, /* Free */ OnReset, /* Reset */ };