Code:text-quads.h

From the change wiki

C header for drawing text in OpenGL. Depends on font bitmap: File:font.data-uint8-1004x19

Code

// text-quads.h
// Functions for drawing text in OpenGL, using textured quads.
#define TQ_TEXTURE_WIDTH 1004
#define TQ_TEXTURE_HEIGHT 19
#define TQ_TEXTURE_FILENAME "font.data-uint8-1004x19" // DEPENDENCY: This texture file.

/*
 Copyright 2022, Elie Goldman Smith

 This program is FREE SOFTWARE: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include <string.h>
typedef struct { float x, y, tx, ty;                 } TQ_Vertex;  // XXX: maybe use 16-bit snorm instead of 32-bit float?
typedef struct { TQ_Vertex *v; int n; int isComplete;} TQ_Drawable;// XXX: in future versions, isComplete might get renamed to 'flags'
TQ_Vertex _tq_alphabet[1024]; // 256 quads (one for every char value)
GLuint    _tq_texture;



void tq_init() {
 // load font bitmap
 size_t SIZE = TQ_TEXTURE_WIDTH*TQ_TEXTURE_HEIGHT;
 unsigned char *bitmap = malloc(SIZE);
 if (!bitmap) return; // TODO: handle error better
 FILE *f = fopen(TQ_TEXTURE_FILENAME,"r");
 if (!f) { perror(TQ_TEXTURE_FILENAME); return; } // TODO: handle error better
 int n = fread(bitmap, 1, SIZE, f);
 if (n != SIZE) { printf("read %d chars\n", n); fclose(f); return; } // TODO: handle error better
 fclose(f);

 // create font texture
 glGenTextures(1, &_tq_texture);
 glEnable(GL_TEXTURE_2D);
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, _tq_texture);
 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, TQ_TEXTURE_WIDTH, TQ_TEXTURE_HEIGHT-1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, &bitmap[TQ_TEXTURE_WIDTH]); // we skip first row of bitmap because that's the indicator for where each character is.  XXX: maybe use a smaller internalformat? we really only need 4-bit monochrome
 glGenerateMipmap(GL_TEXTURE_2D); // XXX: maybe get rid of mipmaps idk
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

 // create the alphabet
 memset(_tq_alphabet, 0, 1024*sizeof(TQ_Vertex));
 int c = 32; // bitmapped font starts at char 32 (space character)
 int lasti = 0;
 for (int i = 0; i<=TQ_TEXTURE_WIDTH; i++) {
  if (bitmap[i]||i==TQ_TEXTURE_WIDTH) {
   float x = (float)(i-lasti)/TQ_TEXTURE_HEIGHT;
   float tx =         (i-0.5f)/TQ_TEXTURE_WIDTH;
   float lasttx = (lasti+0.5f)/TQ_TEXTURE_WIDTH;
   lasti = i;
   _tq_alphabet[c*4  ].x = x;
   _tq_alphabet[c*4  ].y = -1.f;
   _tq_alphabet[c*4  ].tx= tx;
   _tq_alphabet[c*4  ].ty= 1;
   _tq_alphabet[c*4+1].x = x;
   _tq_alphabet[c*4+1].y = 0.f;
   _tq_alphabet[c*4+1].tx= tx;
   _tq_alphabet[c*4+1].ty= 0;
   _tq_alphabet[c*4+2].x = 0;
   _tq_alphabet[c*4+2].y = 0.f;
   _tq_alphabet[c*4+2].tx= lasttx;
   _tq_alphabet[c*4+2].ty= 0;
   _tq_alphabet[c*4+3].x = 0;
   _tq_alphabet[c*4+3].y = -1.f;
   _tq_alphabet[c*4+3].tx= lasttx;
   _tq_alphabet[c*4+3].ty= 1;
   c++;
  }
 }
 free(bitmap);
}



void tq_mode() {
 glEnable(GL_TEXTURE_2D);
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, _tq_texture);
 glEnable(GL_BLEND);
 glBlendFunc(GL_ONE, GL_ONE);
 glEnableClientState(GL_VERTEX_ARRAY);
 glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}



void tq_draw(TQ_Drawable td) {
 glVertexPointer  (2, GL_FLOAT, sizeof(TQ_Vertex), &td.v[0].x);
 glTexCoordPointer(2, GL_FLOAT, sizeof(TQ_Vertex), &td.v[0].tx);
 glDrawArrays(GL_QUADS, 0, td.n);
}



TQ_Drawable tq_centered_fitted(const char *str, float width, float height) { // Nominal font size is 1. Actual font size may vary slightly to fit.
 TQ_Drawable td; td.n=0; td.v=NULL; td.isComplete=0;
 if (!str   )return td; // null string
 if (!str[0])return td; // blank string
 td.v = malloc((strlen(str)+3+1) * 4 * sizeof(TQ_Vertex));
 if (!td.v  )return td; // malloc error
 const float SPACING = 0.05f;
 const char *p = str;
 float       x=0, y=0;
 const char *ls = p; // line start
 const char *le = p; // line end
 float       lw = 0; // line width
 float       widest=0;// widest line
 while (*p && y > -height+0.99f) {
  // determine where the next line should end...
  lw = x = 0;
  while (1) {
   if (*p=='\0'||*p=='\n'){ le=p; lw=x; break; }
   if (isspace(*p))       { le=p; lw=x;        }
   x += _tq_alphabet[*(unsigned char*)p * 4].x + SPACING;
   if (x >= width && lw>0)  {       break; }
   if (*p=='-' || *p==','){ le=p; lw=x;        }
   p++;
  } lw -= SPACING;
  // generate quads of that line...
  if (lw <= width) { // usual case
   x = -0.5f * lw;
   if (widest<lw) widest=lw;
   for (p=ls; p<=le; p++) {
    int base = *(unsigned char*)p * 4;
    if (*p != ' ') { // skipping spaces is an optimization
     for (int i=0;i<4;i++) {
      td.v[td.n] = _tq_alphabet[base+i];
      td.v[td.n].x += x;
      td.v[td.n].y += y;
      td.n++;
     }
    } x += _tq_alphabet[base].x + SPACING;
   } y -= 1.f;
  }
  else { // case where line is one word and too wide: shrink
   x = -0.5f * width;
   widest = width;
   float scale = width/lw;
   float spacing = SPACING*scale;
   for (p=ls; p<=le; p++) {
    int base = *(unsigned char*)p * 4;
    if (*p != ' ') {
     for (int i=0;i<4;i++) {
      td.v[td.n] = _tq_alphabet[base+i];
      td.v[td.n].x *= scale;
      td.v[td.n].x += x;
      td.v[td.n].y *= scale;
      td.v[td.n].y += y;
      td.n++;
     }
    } x += _tq_alphabet[base].x*scale + spacing;
   } y -= scale;
  }
  if (*le == '\0') { td.isComplete = 1; break; }
  ls = p = ++le;
 }
 // expand text if small
 if (widest < width && y > -height) {
  float sx = width/widest;
  float sy = -height/y;
  float scale = sx<sy?sx:sy;
  for (int i=0; i<td.n; i++) {
   td.v[i].x *= scale;
   td.v[i].y *= scale;
  } y *= scale;
 }
 // add ellipsis if text didn't all fit
 if (*le) {
  int base = 4 * (unsigned char)'.';
  for (int h=0;h<3;h++) {
   x = 0.5f*lw + h * (_tq_alphabet[base].x + 0.02f);
   for (int i=0;i<4;i++) {
    td.v[td.n] = _tq_alphabet[base+i];
    td.v[td.n].x += x;
    td.v[td.n].y += y + 1.f;
    td.n++;
   }
  }
 }
 // center vertically
 y *= 0.5f; for (int i=0; i<td.n; i++) td.v[i].y -= y;
 // done
 return td;
}



void tq_delete(TQ_Drawable *td) {
 free(td->v); td->v = NULL;
 td->n = 0; td->isComplete = 0;
}



void tq_done() {
 glDeleteTextures(1, &_tq_texture);
}