1 /** 2 The Sharpest Font Library For D Game Development 3 */ 4 module razor_font; 5 6 import std.conv; 7 import std.file; 8 import std.json; 9 import std.typecons; 10 import color; 11 import png; 12 import std.math; 13 14 // ____________________________ 15 // | RAZOR FONT | 16 // |____________________________| 17 // \ /\ / 18 // / \/ \ 19 // | The Sharpest Font Library | 20 // | For D Game Development | 21 // |____________________________| 22 23 /** 24 Stores IMPORTANT font data to be reused by Razor Font - These are stored in static memory in the program 25 Counts are so we can grab a slice of this information because anything after it WILL be garbage data 26 */ 27 /// The current character limit (letters in string) 28 private immutable int CHARACTER_LIMIT = 4096; 29 /// 4 vec2 (so 8 per char) vertex positions 30 private double[4 * 2 * CHARACTER_LIMIT] vertexCache; 31 // 4 vec4 (so 16 per char) colors - defaults to 0,0,0,1 rgba 32 private double[4 * 4 * CHARACTER_LIMIT] colorCache; 33 34 /// 4 vec2 (so 8 per char) texture coordinate positions 35 private double[8 * CHARACTER_LIMIT] textureCoordinateCache; 36 /// 2 tris (so 6 per char) indices 37 private int[6 * CHARACTER_LIMIT] indicesCache; 38 /// The count of each of these so we can grab a slice of data fresh out of the oven, delicious! 39 private int vertexCount = 0; 40 private int textureCoordinateCount = 0; 41 private int indicesCount = 0; 42 private int colorCount = 0; 43 private int chars = 0; 44 45 /** 46 This allows batch rendering to a "canvas" ala vertex positionining 47 With this you can shovel one giant lump of data into a vao or whatever you're using. 48 This is optional though, you can do whatever you want! 49 */ 50 private double canvasWidth = -1; 51 private double canvasHeight = -1; 52 53 /** 54 These store constant data that is highly repetitive 55 */ 56 private immutable double[8] RAW_VERTEX = [ 0,0, 0,1, 1,1, 1,0 ]; 57 private immutable int[6] RAW_INDICES = [ 0,1,2, 2,3,0 ]; 58 59 /** 60 The offset of the text shadowing. 61 62 Note: Since offset is only proportional to the font size when rendering, 63 the offset is completely detached from the font spec! 64 65 The font spec has no bearing on how the offset is calculated. Only font size. 66 67 0.05 by default because I think it looks nice. :) 68 */ 69 private double shadowOffsetX = 0.05; 70 private double shadowOffsetY = 0.05; 71 72 /** 73 The RGBA components of the shadow 74 */ 75 private double[4] shadowColor = [0,0,0,1]; 76 77 /** 78 Are shadows enabled? 79 80 They get disabled everytime you run renderToCanvas(). 81 This is so there basically isn't a "shadow memory leak". 82 83 As in: Oops I forgot to disable shadows now everything after has a 84 shadow for some reason! 85 */ 86 private bool shadowsEnabled = false; 87 88 /** 89 Allows turning off the shadowing color fill for performance. 90 Say you want a rainbow shadow, you can use this for that. 91 */ 92 private bool shadowColoringEnabled = true; 93 94 /** 95 This is a very simple fix for static memory arrays being filled with no. 96 A simple on switch for initialization. 97 To use RazorFont, you must create a font, so it runs this in there. 98 */ 99 private bool initializedColorArray = false; 100 private void initColorArray() { 101 if (initializedColorArray) { 102 return; 103 } 104 initializedColorArray = true; 105 for (int i = 0; i < 16 * CHARACTER_LIMIT; i += 4) { 106 colorCache[i] = 0; 107 colorCache[i + 1] = 0; 108 colorCache[i + 2] = 0; 109 colorCache[i + 3] = 1; 110 } 111 } 112 113 /** 114 Caches the current font in use. 115 Think of this like the golfball on an IBM Selectric. 116 You can use one ball, type out in one font. Then flush to your render target. 117 Then you can swap to another ball and type in another font. 118 119 Just remember, you must flush or this is going to throw an error because 120 it would create garbage text data without a lock when swapping golfballs, aka fonts. 121 */ 122 private RazorFont currentFont = null; 123 124 /// This stores the current font name as a string 125 private string currentFontName; 126 127 /// This is the lock described in the comment above; 128 private bool fontLock = false; 129 130 /// Stores all fonts 131 private RazorFont[string] razorFonts; 132 133 /// A simple struct to get the font data for the shader 134 struct RazorFontData { 135 double[] vertexPositions; 136 double[] textureCoordinates; 137 int[] indices; 138 double[] colors; 139 } 140 /// A simple struct to get the width and height of rendered text 141 struct RazorTextSize { 142 double width = 0.0; 143 double height = 0.0; 144 } 145 146 // Allows an automatic upload into whatever render target (OpenGL, Vulkan, Metal, DX) as a string file location 147 private void delegate(string) renderTargetAPICallString = null; 148 149 // Allows DIRECT automatic upload into whatever render target (OpenGL, Vulkan, Metal, DX) as RAW data 150 private void delegate(ubyte[], int, int) renderTargetAPICallRAW = null; 151 152 // Allows an automate render into whatever render target (OpenGL, Vulkan, Metal, DX) simply by calling render() 153 private void delegate(RazorFontData) renderApiRenderCall = null; 154 155 156 /** 157 Allows automatic render target (OpenGL, Vulkan, Metal, DX) passthrough instantiation. 158 This can basically pass a file location off to your rendering engine and auto load it into memory. 159 */ 160 void setRenderTargetAPICallString(void delegate(string) apiStringFunction) { 161 if (renderTargetAPICallRAW !is null) { 162 throw new Exception("Razor Font: You already set the RAW api integration function!"); 163 } 164 renderTargetAPICallString = apiStringFunction; 165 } 166 167 168 /** 169 Allows automatic render target (OpenGL, Vulkan, Metal, DX) DIRECT instantiation. 170 This allows the render engine to AUTOMATICALLY upload the image as RAW data. 171 ubyte[] = raw data. int = width. int = height. 172 */ 173 void setRenderTargetAPICallRAW(void delegate(ubyte[], int, int) apiRAWFunction) { 174 if (renderTargetAPICallString !is null) { 175 throw new Exception("Razor Font: You already set the STRING api integration function!"); 176 } 177 renderTargetAPICallRAW = apiRAWFunction; 178 } 179 180 /** 181 Allows automatic render target (OpenGL, Vulkan, Metal, DX) DIRECT rendering via RazorFont. 182 You can simply call render() on the library and it will automatically do whatever you 183 tell it to with this delegate function. This will also automatically run flush(). 184 */ 185 void setRenderFunc(void delegate(RazorFontData) renderApiRenderFunction) { 186 if (renderApiRenderCall !is null) { 187 throw new Exception("Razor Font: You already set the RENDER api integration function!"); 188 } 189 renderApiRenderCall = renderApiRenderFunction; 190 } 191 192 193 // A simple font container 194 private class RazorFont { 195 196 // Font base pallet width (in pixels) 197 int palletWidth = 0; 198 int palletHeight = 0; 199 200 // Pixel space (literally) between characters in pallet 201 int border = 0; 202 203 // Number of characters (horizontal, aka X) 204 int rows = 0; 205 206 // How far the letters are from each other 207 double spacing = 1.0; 208 209 // How big the space character is (' ') 210 double spaceCharacterSize = 4.0; 211 212 // Character pallet (individual) in pixels 213 int characterWidth = 0; 214 int charactertHeight = 0; 215 216 // Readonly specifier if kerning was enabled 217 bool kerned = false; 218 219 // Readonly specifier if trimming was enabled 220 bool trimmedX = false; 221 bool trimmedY = false; 222 223 // Readonly directory for texture (entire, including the .png) 224 string fileLocation; 225 226 // Character map - stored as a linear associative array for O(1) retrieval 227 /** 228 Stores as: 229 [ 230 -x -y, 231 -x +y, 232 +x +y, 233 +x -y 234 ] 235 or this, if it's easier to understand: 236 [ 237 top left, 238 bottom left, 239 bottom right, 240 top right 241 ] 242 GPU optimized vertex positions! 243 244 Accessed as: 245 double[] myCoolBlah = map["whatever letter/unicode thing you're getting"]; 246 247 The last 1 values specify width of the character 248 */ 249 double[9][dchar] map; 250 251 // Stores the map raw as a linear array before processed 252 string rawMap; 253 } 254 255 /** 256 Create a font from your PNG JSON pairing in the directory. 257 258 You do not specify an extension. 259 260 So if you have: cool.png and cool.json 261 You would call this as: createFont("fonts/cool") 262 263 Name is an optional. You will call into Razor Font by this name. 264 265 If you do not specify a name, you must call into Razor Font by the fileLocation, literal. 266 267 If you turn on trimming, your font will go from monospace to proportional. 268 269 Spacing is how far the letters are from each other. Default: 1.0 pixel 270 271 spaceCharacterSize is how big the ' ' (space) character is. By default, it's 4 pixels wide. 272 */ 273 void createFont(string fileLocation, string name = "", bool trimming = false, double spacing = 1.0, double spaceCharacterSize = 4.0) { 274 275 // This is the fix explained above 276 initColorArray(); 277 278 //! Place holder for future 279 bool kerning = false; 280 281 // Are we using the fileLocation as the key, or did they specify a name? 282 const string key = name == "" ? fileLocation : name; 283 284 const string pngLocation = fileLocation ~ ".png"; 285 const string jsonLocation = fileLocation ~ ".json"; 286 287 // Make sure the files exist 288 checkFilesExist(pngLocation, jsonLocation); 289 290 // Automate existing engine integration 291 tryCallingRAWApi(pngLocation); 292 tryCallingStringApi(pngLocation); 293 294 // Create the Font object 295 RazorFont fontObject = new RazorFont(); 296 297 // Store the file location in the object 298 fontObject.fileLocation = pngLocation; 299 300 // Now parse the json, and pass it into object 301 parseJson(fontObject, jsonLocation); 302 303 // Now encode the linear string as a keymap of raw graphics positions 304 encodeGraphics(fontObject, kerning, trimming, spacing, spaceCharacterSize); 305 306 // Finally add it into the library 307 razorFonts[key] = fontObject; 308 309 } 310 311 //* ============================ BEGIN GRAPHICS DISPATCH =========================== 312 313 /** 314 Allows you to blanket set the color for the entire canvas. 315 316 Be careful though, this overwrites the entire color cache 317 after the currently rendered character position in memory! 318 */ 319 void switchColors(double r, double g, double b, double a = 1.0) { 320 for (int i = colorCount; i < colorCache.length; i += 4) { 321 colorCache[i] = r; 322 colorCache[i + 1] = g; 323 colorCache[i + 2] = b; 324 colorCache[i + 3] = a; 325 } 326 } 327 328 /** 329 Allows you to set the offet of the text shadowing. 330 331 This is RELATIVE via the font size so it will remain consistent 332 across any font size! 333 334 Remember: Offset will become reset to default when you call renderToCanvas() 335 */ 336 void setShadowOffset(double x, double y) { 337 shadowOffsetX = x / 10.0; 338 shadowOffsetY = y / 10.0; 339 } 340 341 /** 342 Allows you to blanket set the shadow color for the entire canvas after the current character. 343 344 Remember: When you renderToCanvas() shadow colors will default back to black. 345 */ 346 void switchShadowColor(double r, double g, double b, double a = 1.0) { 347 shadowColor[0] = r; 348 shadowColor[1] = g; 349 shadowColor[2] = b; 350 shadowColor[3] = a; 351 } 352 353 354 /** 355 Allows you to blanket a range of characters in the canvas with a color. 356 357 So if you have: abcdefg 358 And run setColorRange(0.5,0.5,0.5, 1, 3, 5) 359 Now e and f are gray. Alpha 1.0 360 */ 361 void setColorRange(int start, int end, double r, double g, double b, double a) { 362 for (int i = start * 16; i < end * 16; i += 4) { 363 colorCache[i] = r; 364 colorCache[i + 1] = g; 365 colorCache[i + 2] = b; 366 colorCache[i + 3] = a; 367 } 368 } 369 370 /** 371 Allows you to set individual character colors 372 */ 373 void setColorChar(int charIndex, double r, double g, double b, double a = 1.0) { 374 const int startIndex = charIndex * 16; 375 for (int i = startIndex; i < startIndex + 16; i += 4) { 376 colorCache[i] = r; 377 colorCache[i + 1] = g; 378 colorCache[i + 2] = b; 379 colorCache[i + 3] = a; 380 } 381 } 382 383 /** 384 Allows you to directly work on vertex position colors in a character. 385 Using direct points (verbose) 386 */ 387 void setColorPoints( 388 int charIndex, 389 390 double topLeftR, 391 double topLeftG, 392 double topLeftB, 393 double topLeftA, 394 395 double bottomLeftR, 396 double bottomLeftG, 397 double bottomLeftB, 398 double bottomLeftA, 399 400 double bottomRightR, 401 double bottomRightG, 402 double bottomRightB, 403 double bottomRightA, 404 405 double topRightR, 406 double topRightG, 407 double topRightB, 408 double topRightA 409 ) { 410 const int startIndex = charIndex * 16; 411 412 // It's already immensely verbose, let's just add on to this verbosity 413 414 colorCache[startIndex] = topLeftR; 415 colorCache[startIndex + 1] = topLeftG; 416 colorCache[startIndex + 2] = topLeftB; 417 colorCache[startIndex + 3] = topLeftA; 418 419 colorCache[startIndex + 4] = bottomLeftR; 420 colorCache[startIndex + 5] = bottomLeftG; 421 colorCache[startIndex + 6] = bottomLeftB; 422 colorCache[startIndex + 7] = bottomLeftA; 423 424 colorCache[startIndex + 8] = bottomRightR; 425 colorCache[startIndex + 9] = bottomRightG; 426 colorCache[startIndex + 10] = bottomRightB; 427 colorCache[startIndex + 11] = bottomRightA; 428 429 colorCache[startIndex + 12] = topRightR; 430 colorCache[startIndex + 13] = topRightG; 431 colorCache[startIndex + 14] = topRightB; 432 colorCache[startIndex + 15] = topRightA; 433 } 434 435 /** 436 Allows you to directly work on vertex position colors in a character. 437 Using direct points (tidy). 438 double vec is [R,G,B,A] 439 */ 440 void setColorPoints(int charIndex, double[4] topLeft, double[4] bottomLeft, double[4] bottomRight, double[4] topRight) { 441 const int startIndex = charIndex * 16; 442 foreach(externalIndex, vec4; [topLeft, bottomLeft, bottomRight, topRight]) { 443 foreach (index, value; vec4) { 444 colorCache[startIndex + (externalIndex * 4) + index] = value; 445 } 446 } 447 } 448 449 /// Allows you to get the max amount of characters allowed in canvas 450 int getMaxChars() { 451 return CHARACTER_LIMIT; 452 } 453 454 /** 455 Allows you to index the current amount of characters on the canvas. This does 456 not include spaces and carriage returns. You MUST call renderToCanvas before 457 calling this otherwise this will always be 0 when you call it. 458 */ 459 int getCurrentCharacterIndex() { 460 return chars; 461 } 462 463 /** 464 Allows you to extract the current font PNG file location automatically 465 */ 466 string getCurrentFontTextureFileLocation() { 467 if (currentFont is null) { 468 throw new Exception("Razor Font: Can't get a font file location! You didn't select one!"); 469 } 470 return currentFont.fileLocation; 471 } 472 473 /** 474 Turns on shadowing. 475 476 Rememeber: This creates twice as many characters because 477 you have to render a background, then a foreground. 478 479 You can also do some crazy stuff with shadows because the shadow 480 colors are stored in the same color cache as regular text. 481 482 Remember: When you renderToCanvas() shadows turn off. 483 */ 484 void enableShadows() { 485 shadowsEnabled = true; 486 } 487 488 489 /// Allows you to render to a canvas using top left as a base position 490 void setCanvasSize(double width, double height) { 491 // Dividing by 2.0 because my test environment shader renders to center on pos(0,0) top left 492 canvasWidth = width / 2.0; 493 canvasHeight = height / 2.0; 494 } 495 496 /** 497 Automatically flushes out the cache, handing the data structure off to 498 the delegate function you defined via setRenderFunc() 499 */ 500 void render() { 501 if (renderApiRenderCall is null) { 502 throw new Exception("Razor Font: You did not set a render api call!"); 503 } 504 505 renderApiRenderCall(flush()); 506 } 507 508 509 /// Flushes out the cache, gives you back a font struct containing the raw data 510 RazorFontData flush() { 511 512 fontLock = false; 513 514 RazorFontData returningStruct = RazorFontData( 515 vertexCache[0..vertexCount], 516 textureCoordinateCache[0..textureCoordinateCount], 517 indicesCache[0..indicesCount], 518 colorCache[0..colorCount] 519 ); 520 521 // Reset the counters 522 vertexCount = 0; 523 textureCoordinateCount = 0; 524 indicesCount = 0; 525 colorCount = 0; 526 chars = 0; 527 528 return returningStruct; 529 } 530 531 /// Allows you to get text size to do interesting things. Returns as RazorTextSize struct 532 RazorTextSize getTextSize(double fontSize, string text) { 533 double accumulatorX = 0.0; 534 double accumulatorY = 0.0; 535 // Cache spacing 536 const double spacing = currentFont.spacing * fontSize; 537 // Cache space (' ') character 538 const double spaceCharacterSize = currentFont.spaceCharacterSize * fontSize; 539 540 // Can't get the size if there's no font! 541 if (currentFont is null) { 542 throw new Exception("Razor Font: Tried to get text size without selecting a font! " ~ 543 "You must select a font before getting the size of text with it!"); 544 } 545 546 foreach (key, character; text) { 547 548 // Skip space 549 if (character == ' ') { 550 accumulatorX += spaceCharacterSize; 551 continue; 552 } 553 // Move down 1 space Y 554 if (character == '\n') { 555 accumulatorY += fontSize; 556 continue; 557 } 558 559 // Skip unknown character 560 if (character !in currentFont.map) { 561 continue; 562 } 563 564 // Font stores character width in index 9 (8 [0 count]) 565 accumulatorX += (currentFont.map[character][8] * fontSize) + spacing; 566 } 567 568 // Add a last bit of the height offset 569 accumulatorY += fontSize; 570 // Remove the last bit of spacing 571 accumulatorX -= spacing; 572 573 // Finally, if shadowing is enabled, add in shadowing offset 574 if (shadowsEnabled) { 575 accumulatorX += (shadowOffsetX * fontSize); 576 accumulatorY += (shadowOffsetY * fontSize); 577 } 578 579 return RazorTextSize(accumulatorX, accumulatorY); 580 } 581 582 /** 583 Selects and caches the font of your choosing. 584 585 Remember: You must flush the cache before choosing a new font. 586 587 This is done because all fonts are different. It would create garbage 588 data on screen without this. 589 */ 590 void selectFont(string font) { 591 592 if (fontLock) { 593 throw new Exception("You must flush() out the cache before selecting a new font!"); 594 } 595 596 // Can't render if that font doesn't exist 597 if (font !in razorFonts) { 598 throw new Exception(font ~ " is not a registered font!"); 599 } 600 601 // Now store and lock 602 currentFont = razorFonts[font]; 603 currentFontName = font; 604 fontLock = true; 605 } 606 607 /** 608 Render to the canvas. Remember: You must run flush() to collect this canvas. 609 If rounding is enabled, it will attempt to keep your text aligned with the pixels on screen 610 to avoid wavy/blurry/jagged text. This will automatically render shadows for you as well. 611 */ 612 void renderToCanvas(double posX, double posY, const double fontSize, string text, bool rounding = true) { 613 614 // Keep square pixels 615 if (rounding) { 616 posX = round(posX); 617 posY = round(posY); 618 } 619 620 // Can't render if no font is selected 621 if (currentFont is null) { 622 throw new Exception("Razor Font: Tried to render without selecting a font! " ~ 623 "You must select a font before rendering to canvas!"); 624 } 625 626 // Can't render to canvas if there IS no canvas 627 if (canvasWidth == -1 && canvasHeight == -1) { 628 throw new Exception("Razor Font: You have to set the canvas size to render to it!"); 629 } 630 631 // Store how far the arm has moved to the right 632 double typeWriterArmX = 0.0; 633 // Store how far the arm has moved down 634 double typeWriterArmY = 0.0; 635 636 // Top left of canvas is root position (X: 0, y: 0) 637 const positionX = posX - canvasWidth; 638 const positionY = posY - canvasHeight; 639 640 // Cache spacing 641 const double spacing = currentFont.spacing * fontSize; 642 643 // Cache space (' ') character 644 const double spaceCharacterSize = currentFont.spaceCharacterSize * fontSize; 645 646 foreach (key, const(dchar) character; text) { 647 648 // Skip space 649 if (character == ' ') { 650 typeWriterArmX += spaceCharacterSize; 651 continue; 652 } 653 // Move down 1 space Y and to space 0 X 654 if (character == '\n') { 655 typeWriterArmY += fontSize; 656 typeWriterArmX = 0.0; 657 continue; 658 } 659 660 // Skip unknown character 661 if (character !in currentFont.map) { 662 continue; 663 } 664 665 // Font stores character width in index 9 (8 [0 count]) 666 double[9] rawData = currentFont.map[character]; 667 668 // Keep on the stack 669 double[8] textureData = rawData[0..8]; 670 //Now dispatch into the cache 671 for (int i = 0; i < 8; i++) { 672 textureCoordinateCache[i + textureCoordinateCount] = textureData[i]; 673 } 674 675 // This is the width of the character 676 // Keep on the stack 677 double characterWidth = rawData[8]; 678 679 // Keep this on the stack 680 double[8] rawVertex = RAW_VERTEX; 681 682 683 // ( 0 x 1 y 2 x 3 y ) <- left side ( 4 x 5 y 6 x 7 y ) <- right side is goal 684 // Now apply trimming 685 for (int i = 4; i < 8; i += 2) { 686 rawVertex[i] = characterWidth; 687 } 688 689 // Now scale 690 foreach (ref double vertexPosition; rawVertex) { 691 vertexPosition *= fontSize; 692 } 693 694 // Shifting 695 for (int i = 0; i < 8; i += 2) { 696 // Now shift right 697 rawVertex[i] += typeWriterArmX + positionX; 698 // Now shift down 699 rawVertex[i + 1] += typeWriterArmY + positionY; 700 } 701 702 typeWriterArmX += (characterWidth * fontSize) + spacing; 703 704 // vertexData ~= rawVertex; 705 // Now dispatch into the cache 706 for (int i = 0; i < 8; i++) { 707 vertexCache[i + vertexCount] = rawVertex[i]; 708 } 709 710 // Keep this on the stack 711 int[6] rawIndices = RAW_INDICES; 712 foreach (ref value; rawIndices) { 713 // Using vertexCount because we're targeting vertex positions 714 value += vertexCount / 2; 715 } 716 // Now dispatch into the cache 717 for (int i = 0; i < 6; i++) { 718 indicesCache[i + indicesCount] = rawIndices[i]; 719 } 720 721 // Now hold cursor position (count) in arrays 722 vertexCount += 8; 723 textureCoordinateCount += 8; 724 indicesCount += 6; 725 colorCount += 16; 726 // This one is characters literal 727 chars++; 728 729 if (vertexCount >= CHARACTER_LIMIT || indicesCount >= CHARACTER_LIMIT) { 730 throw new Exception("Character limit is: " ~ to!string(CHARACTER_LIMIT)); 731 } 732 } 733 734 /** 735 Because there is no Z buffer in 2d, OpenGL seems to NOT overwrite pixel data of existing 736 framebuffer pixels. Since this is my testbed, I must assume that this is how 737 Vulkan, Metal, DX, and so-on do this. This is GUARANTEED to not affect software renderers. 738 So we have to do the shadowing AFTER the foreground. 739 740 We need to poll, THEN disable the shadow variable because without that it would be 741 an infinite recursion, aka a stack overflow. 742 */ 743 const bool shadowsWereEnabled = shadowsEnabled; 744 shadowsEnabled = false; 745 if (shadowsWereEnabled) { 746 const int textLength = getTextRenderableCharsLength(text); 747 const int currentIndex = getCurrentCharacterIndex(); 748 if (shadowColoringEnabled) { 749 setColorRange( 750 currentIndex, 751 currentIndex + textLength, 752 shadowColor[0], 753 shadowColor[1], 754 shadowColor[2], 755 shadowColor[3] 756 ); 757 } 758 renderToCanvas(posX + (shadowOffsetX * fontSize), posY + (shadowOffsetY * fontSize), fontSize, text, false); 759 760 shadowOffsetX = 0.05; 761 shadowOffsetY = 0.05; 762 } 763 764 // Turn this back on because it can become a confusing nightmare 765 shadowColoringEnabled = true; 766 // Switch back to black because this also can become a confusing nightmare 767 switchShadowColor(0,0,0); 768 } 769 770 /** 771 Processes your input string, then sends you how long it would be when rendering. 772 Helpful for repositioning your "cursor" in the texture cache! 773 774 Note: This will return cursor position into the beginning index of the background 775 of the shadowed text if you're using it for subtraction. 776 */ 777 int getTextRenderableCharsLength(string input) { 778 import std.array; 779 return cast(int)input.replace(" ", "").replace("\n", "").length; 780 } 781 782 /** 783 Processes your input text string with shadows to see how long it would be when rendering. 784 Helpful for positioning your "cursor" in the texture cache! 785 786 Note: This will return cursor position into the beginning index of the foreground 787 of the shadowed text if you're using it for subtraction. 788 */ 789 int getTextRenderableCharsLengthWithShadows(string input) { 790 return getTextRenderableCharsLength(input) * 2; 791 } 792 793 /** 794 Allows you to disable shadow coloring for a teeny tiny bit of performance 795 when you're doing cool custom shadow coloring! 796 797 Important Note: When renderToCanvas() is called, shadow coloring is turned 798 back on because it can become a confusing nightmare if not done like this. 799 */ 800 void disableShadowColoring() { 801 shadowColoringEnabled = false; 802 } 803 804 /** 805 Allows you to manually move around characters. 806 807 Note: You can manually move around shadows by getting the 808 renderable text size before turning on shadows, then offset 809 your current index into the string by this size. 810 811 Note: This is in pixel coordinates. 812 */ 813 void moveChar(int index, double posX, double posY) { 814 // This gets a bit confusing, so I'm going to write it out verbosely to be able to read/maintain it 815 816 // Move to cursor position in vertexCache 817 const int baseIndex = index * 8; 818 819 // Top left 820 vertexCache[baseIndex ] += posX; // X 821 vertexCache[baseIndex + 1] -= posY; // Y 822 823 // Bottom left 824 vertexCache[baseIndex + 2] += posX; // X 825 vertexCache[baseIndex + 3] -= posY; // Y 826 827 // Bottom right 828 vertexCache[baseIndex + 4] += posX; // X 829 vertexCache[baseIndex + 5] -= posY; // Y 830 831 // Top right 832 vertexCache[baseIndex + 6] += posX; // X 833 vertexCache[baseIndex + 7] -= posY; // Y 834 } 835 836 /** 837 Rotate a character around the centerpoint of it's face. 838 839 Note: This defaults to radians by default. 840 841 Note: If you use moveChar() with this, you MUST do moveChar() first! 842 */ 843 void rotateChar(int index, double rotation, bool isDegrees = false) { 844 845 // This is why my doml is required 846 import doml.vector_3d; 847 import doml.matrix_4d; 848 849 if (isDegrees) { 850 immutable radToDegrees = 180.0 / PI; 851 rotation *= radToDegrees; 852 } 853 854 /** 855 This is written out even more verbosely than moveChar() 856 so you can see why you must do moveChar() first. 857 */ 858 859 // Move to cursor position in vertexCache 860 const int baseIndex = index * 8; 861 862 // Convert to 3d to suppliment to 4x4 matrix 863 Vector3d topLeft = Vector3d(vertexCache[baseIndex ], vertexCache[baseIndex + 1], 0); 864 Vector3d bottomLeft = Vector3d(vertexCache[baseIndex + 2], vertexCache[baseIndex + 3], 0); 865 Vector3d bottomRight = Vector3d(vertexCache[baseIndex + 4], vertexCache[baseIndex + 5], 0); 866 Vector3d topRight = Vector3d(vertexCache[baseIndex + 6], vertexCache[baseIndex + 7], 0); 867 868 Vector3d centerPoint = Vector3d((topLeft.x + topRight.x) / 2.0, (topLeft.y + bottomLeft.y) / 2.0, 0); 869 870 Vector3d topLeftDiff = Vector3d(topLeft) .sub(centerPoint); 871 Vector3d bottomLeftDiff = Vector3d(bottomLeft) .sub(centerPoint); 872 Vector3d bottomRighttDiff = Vector3d(bottomRight).sub(centerPoint); 873 Vector3d topRighttDiff = Vector3d(topRight) .sub(centerPoint); 874 875 // These calculations also store the new data in the variables we created above 876 // We must center the coordinates into real coordinates 877 878 Matrix4d().rotate(rotation, 0,0,1).translate(topLeftDiff) .getTranslation(topLeft); 879 Matrix4d().rotate(rotation, 0,0,1).translate(bottomLeftDiff) .getTranslation(bottomLeft); 880 Matrix4d().rotate(rotation, 0,0,1).translate(bottomRighttDiff).getTranslation(bottomRight); 881 Matrix4d().rotate(rotation, 0,0,1).translate(topRighttDiff) .getTranslation(topRight); 882 883 884 topLeft.x += centerPoint.x; 885 topLeft.y += centerPoint.y; 886 887 bottomLeft.x += centerPoint.x; 888 bottomLeft.y += centerPoint.y; 889 890 bottomRight.x += centerPoint.x; 891 bottomRight.y += centerPoint.y; 892 893 topRight.x += centerPoint.x; 894 topRight.y += centerPoint.y; 895 896 vertexCache[baseIndex ] = topLeft.x; 897 vertexCache[baseIndex + 1] = topLeft.y; 898 899 vertexCache[baseIndex + 2] = bottomLeft.x; 900 vertexCache[baseIndex + 3] = bottomLeft.y; 901 902 vertexCache[baseIndex + 4] = bottomRight.x; 903 vertexCache[baseIndex + 5] = bottomRight.y; 904 905 vertexCache[baseIndex + 6] = topRight.x; 906 vertexCache[baseIndex + 7] = topRight.y; 907 } 908 909 //! ============================ END GRAPHICS DISPATCH ============================= 910 911 //* ========================= BEGIN GRAPHICS ENCODING ============================== 912 913 private void encodeGraphics(ref RazorFont fontObject, bool kerning, bool trimming, double spacing, double spaceCharacterSize) { 914 915 // Store all this on the stack 916 917 // Total image size 918 const double palletWidth = cast(double)fontObject.palletWidth; 919 const double palletHeight = cast(double)fontObject.palletHeight; 920 921 // How many characters (width, then height) 922 const int rows = fontObject.rows; 923 924 // How wide and tall are the characters in pixels 925 const int characterWidth = fontObject.characterWidth; 926 const int characterHeight = fontObject.charactertHeight; 927 928 // The border between the characters in pixels 929 const int border = fontObject.border; 930 931 // Store font spacing here as it's a one shot operation 932 fontObject.spacing = spacing / characterWidth; 933 934 // Store space character width as it's a one shot operation 935 fontObject.spaceCharacterSize = spaceCharacterSize / characterWidth; 936 937 // Cache a raw true color image for trimming if requested 938 const TrueColorImage tempImageObject = trimming == false ? null : readPng(fontObject.fileLocation).getAsTrueColorImage(); 939 940 foreach (size_t i, const(dchar) value; fontObject.rawMap) { 941 942 // Starts off as a normal monospace size 943 int thisCharacterWidth = characterWidth; 944 945 // Turn off annoying casting suggestions 946 const int index = cast(int) i; 947 948 // Now get where the typewriter is 949 const int currentRow = index % rows; 950 const int currentColum = index / rows; 951 952 // Now get literal pixel position (top left) 953 int intPosX = (characterWidth + border) * currentRow; 954 int intPosY = (characterHeight + border) * currentColum; 955 956 // left top, 957 // left bottom, 958 // right bottom, 959 // right top 960 961 // Now calculate limiters 962 // +1 on max because the GL texture stops on the top left of the point in the texture pixel 963 int minX = intPosX; 964 int maxX = intPosX + characterWidth + 1; 965 966 const int minY = intPosY; 967 const int maxY = intPosY + characterHeight + 1; 968 969 // Now trim it if requested 970 if (trimming) { 971 972 // Create temp workers 973 int newMinX = minX; 974 int newMaxX = maxX; 975 976 // Trim left side 977 outer1: foreach(x; minX..maxX) { 978 newMinX = x; 979 foreach (y; minY..maxY) { 980 // This is ubyte (0-255) 981 if (tempImageObject.getPixel(x,y).a > 0) { 982 break outer1; 983 } 984 } 985 } 986 987 // Trim right side 988 outer2: foreach_reverse(x; minX..maxX) { 989 // +1 because of the reason stated above assigning minX and maxX 990 newMaxX = x + 1; 991 foreach (y; minY..maxY) { 992 // This is ubyte (0-255) 993 if (tempImageObject.getPixel(x,y).a > 0) { 994 break outer2; 995 } 996 } 997 } 998 999 // I was going to throw a blank space check, but maybe someone has a reason for that 1000 1001 minX = newMinX; 1002 maxX = newMaxX; 1003 1004 thisCharacterWidth = maxX - minX; 1005 1006 } 1007 1008 // Now shovel it into a raw array so we can easily use it - iPos stands for Integral Positions 1009 // -1 on maxY because the position was overshot, now we reverse it 1010 int[] iPos = [ 1011 minX, minY, // Top left 1012 minX, maxY - 1, // Bottom left 1013 maxX, maxY - 1, // Bottom right 1014 maxX, minY, // Top right 1015 1016 thisCharacterWidth, // Width 1017 ]; 1018 1019 // Now calculate REAL graphical texture map 1020 double[9] glPositions = [ 1021 iPos[0] / palletWidth, iPos[1] / palletHeight, 1022 iPos[2] / palletWidth, iPos[3] / palletHeight, 1023 iPos[4] / palletWidth, iPos[5] / palletHeight, 1024 iPos[6] / palletWidth, iPos[7] / palletHeight, 1025 1026 // Now store char width - Find the new double size by comparing it to original 1027 // Will simply be 1.0 with monospaced fonts 1028 cast(double)iPos[8] / cast(double)characterWidth 1029 ]; 1030 1031 // Now dump it into the dictionary 1032 fontObject.map[value] = glPositions; 1033 } 1034 } 1035 1036 1037 1038 1039 1040 //! ========================= END GRAPICS ENCODING ================================ 1041 1042 1043 //* ========================== BEGIN JSON DECODING ================================== 1044 // Run through the required data to assemble a font object 1045 private void parseJson(ref RazorFont fontObject, const string jsonLocation) { 1046 void[] rawData = read(jsonLocation); 1047 string jsonString = cast(string)rawData; 1048 JSONValue jsonData = parseJSON(jsonString); 1049 1050 foreach (string key,JSONValue value; jsonData.objectNoRef) { 1051 switch(key) { 1052 case "pallet_width": { 1053 assert(value.type == JSONType.integer); 1054 fontObject.palletWidth = cast(int)value.integer; 1055 break; 1056 } 1057 case "pallet_height": { 1058 assert(value.type == JSONType.integer); 1059 fontObject.palletHeight = cast(int)value.integer; 1060 break; 1061 } 1062 case "border": { 1063 assert(value.type == JSONType.integer); 1064 fontObject.border = cast(int)value.integer; 1065 break; 1066 } 1067 case "rows": { 1068 assert(value.type == JSONType.integer); 1069 fontObject.rows = cast(int)value.integer; 1070 break; 1071 } 1072 case "character_width": { 1073 assert(value.type == JSONType.integer); 1074 fontObject.characterWidth = cast(int)value.integer; 1075 break; 1076 } 1077 case "charactert_height": { 1078 assert(value.type == JSONType.integer); 1079 fontObject.charactertHeight = cast(int)value.integer; 1080 break; 1081 } 1082 case "character_map": { 1083 assert(value.type == JSONType..string); 1084 fontObject.rawMap = value.str; 1085 break; 1086 } 1087 default: // Unknown 1088 } 1089 } 1090 } 1091 1092 1093 //!============================ END JSON DECODING ================================== 1094 1095 //* ========================== BEGIN API AGNOSTIC CALLS ============================ 1096 // Attempts to automate the api RAW call 1097 private void tryCallingRAWApi(string fileLocation) { 1098 if (renderTargetAPICallRAW is null) { 1099 return; 1100 } 1101 1102 // Use ADR's awesome framework library to convert the png into a raw data stream. 1103 TrueColorImage tempImageObject = readPng(fileLocation).getAsTrueColorImage(); 1104 1105 const int width = tempImageObject.width(); 1106 const int height = tempImageObject.height(); 1107 1108 renderTargetAPICallRAW(tempImageObject.imageData.bytes, width, height); 1109 } 1110 1111 // Attemps to automate the api String call 1112 private void tryCallingStringApi(string fileLocation) { 1113 if (renderTargetAPICallString is null) { 1114 return; 1115 } 1116 1117 renderTargetAPICallString(fileLocation); 1118 } 1119 1120 //! ======================= END API AGNOSTIC CALLS ================================ 1121 1122 //* ===================== BEGIN ETC FUNCTIONS =============================== 1123 1124 1125 // Makes sure there's data where there should be 1126 private void checkFilesExist(string pngLocation, string jsonLocation) { 1127 if (!exists(pngLocation)) { 1128 throw new Exception("Razor Font: " ~ pngLocation ~ " does not exist!"); 1129 } 1130 1131 if (!exists(jsonLocation)) { 1132 throw new Exception("Razor Font: " ~ jsonLocation ~ " does not exist!"); 1133 } 1134 } 1135 1136 //! ===================== END ETC FUNCTIONS =====================================