Las fuentes de letras son la base de la información textual y muchas veces se hace imprescindible para el programador ofrecerle al usuario una serie de datos e informaciones, tales como pueda ser un menú de ayuda, cualquier texto de pantalla, información en los botones del entorno gráfico, etc. En los modos gráficos ya no disponemos de la fuente de letras de que disponíamos en el modo de texto, así que tenemos que ingeniárnoslas para, a partir de las primitvas gráficas de que disponemos, representar en pantalla el equivalente a los símbolos gráficos que representan las letras.
En anteriores artículos vimos la base de la programación gráfica con el desarrollo de la función PutPixel(), que representaba un punto en pantalla con el color especificado. En principio, el manejo de esta función va a ser la base de las fuentes de letra, pero ahora introduciendo en nuestros ejemplos más ensamblador proporcionando un mejor resultado en cuanto a velocidad de ejecución y eficiencia se refiere.
En este sentido de aplicación al MS-DOS, a medida que el curso avance, se irá eliminando código C y añadiendo nuevas instrucciones ASM que dotaran a nuestros programas de mejores resultados. Como siempre dispondremos del pseudocódigo de cada función, será sencillo portarlo a cualquier otro sistema simplemente cambiando la función más interna, la específica de cada sistema operativo. Esto quiere decir, en pocas palabras, que si queremos portar la rutina de dibujar sprites a Linux, por ejemplo, tan sólo habremos de sustituir la rutina PutPixel() en asm que habíamos creado para DOS por otra específica para Linux. De esta manera con un pequeño cambio podemos adaptar todas las rutinas y luego optimizarlas para cada sistema.
Como muchos ya habrán intuido, vamos a tratar cada letra como un bloque de pixels (como un bitmap o sprite), de manera que podremos guardarlas en arrays y dibujarlas en pantalla con uso de bucles anidados.
Lo anterior viene a decir que nosotros podemos crearnos nuestra propia definición de la letra 'A' (por poner un ejemplo) definiéndola como si fuera un sprite y dibujarla en pantalla de igual manera que hicimos en el número anterior:
El caracter "A": ...XXXX... .XXX..XXX. XXX....XXX XXXXXXXXXX XXXXXXXXXX XXX....XXX XXX....XXX XXX....XXXEste carácter que nosotros hemos dibujado con distintos puntos (representados por las X) y por huecos donde no se dibuja (en este caso los puntos '.'), podemos transformarlo a códigos de colores comprensibles por el ordenador. Supongamos que queremos una letra azul (color 1) con fondo negro (color 0). Nuestro anterior caracter viene a ser representado ahora como:
El caracter "A": 0001111000 0111001110 1110000111 1111111111 1111111111 1110000111 1110000111 1110000111Al transformalo en un array, quedaría almacenado así:
char Letra[10][8]= {0,0,0,1,1,1,1,0,0,0,0,1,1,1,0,0,1,1,1,0,etc ... }Ya tenemos una letra guardada en el array. ¿Cómo la dibujamos en pantalla? Muy sencillo: supongamos inicializado cualquier modo gráfico (ya sea 320x200, 640x480 ó 1024x768) y disponiendo de una función PutPixel(x,y,color) que dibuje el punto (x,y) con el color especificado. Dibujar la letra en la posición (160,100) (por ejemplo) se reduce a dibujar todos los pixels que estén activados (con el color 1) y no dibujar nada en los que no deban ser modificados (el fondo de la letra, que es el color 0 en este caso):
#define ANCHO 10 #define ALTO 8 for( by=0; by<ALTO; by++) for( bx=0; bx<ANCHO; bx++) PutPixel(160+bx, 100+by, Letra[bx][by]);Nótese que podemos crear las letras del tamaño que deseemos, de 8x16, de 16x16, etc. Así eliminamos el problema de los modos de texto que disponían de un tipo de letra con un tamaño fijo. Ahora podemos representar en pantalla las fuentes del tamaño y tipo que queramos. Podríamos haber dibujado la A inclinada en el array (con lo que representaría una letra cursiva), podíamos haberla dibujado más rellena (negrita), usar otros estilos de letra, etc. En modo gráfico disponemos de la libertad de creación con la resolución de pantalla como única limitación. Cualquier forma gráfica, siempre que quepa en la pantalla, está en nuestras manos.
En el ejemplo anterior hemos anidado dos bucles de manera que en su interior contenía la instrucción PutPixel() que dibujaba el pixel correspondiente del array bidimensional en pantalla. La letra ha sido guardada en un array bidimensional que contiene <anchoxalto> pixels. También es posible guardar una letra en un array unidimensional, cuya ventaja es la de disponer de los pixels que forman la letra de manera más intuitivamente lineal en memoria. Los arrays bidimensionales son más sencillos de leer (en cuanto a lectura y compresión del código se refiere), pero cuando nos encontremos trabajando con distintos modos gráficos es más conveniente disponer de memoria lineal y acceder a estos arrays con la formula (ya bastante conocida por los lectores) de:
Color (x,y) del array = Array[Ancho*y+x]Esto nos va a beneficiar en gran parte a la optimización del código que queramos desarrollar, y siguen siendo igual de legibles que los bidimensionales:
Array unidimensional: char Letra[ancho*alto] = { 0,0,1,1,1,1,0,0, 0,1,1,0,0,1,1,0, 1,1,0,0,0,0,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,0,0,0,0,1,1, 1,1,0,0,0,0,1,1, 1,1,0,0,0,0,1,1 };De modo que a partir de ahora la mayoría de los ejemplos y listados estarán orientados mediante el uso de arrays unidimensionales, dada sencillez y linealidad en memoria.
Cada una de estas letras están guardadas de la manera en que guardábamos la letra 'A' en el array de memoria, pero ahora en lugar de guardar una sóla letra, guardamos todas las que contiene la tabla de carácteres. De esta manera a la hora de desarrollar un programa seremos capaces de dibujar tanto carácteres individuales como frases completas, éstas últimas dibujando todos los carácteres que la componen consecutivamente en pantalla.
El modo texto nos limita a utilizar el único estilo de letra que existe (aunque puede cambiarse como veremos al final de este texto), pero en los modos gráficos podemos crear nuestros propios estilos que harán diferenciar a nuestros programas de los demás.
Hasta ahora hemos generalizado con las fuentes como conjunto de letras almacenadas en un array o fichero, donde cada uno de los carácteres está compuesto de tantos colores como de ancho x alto tenga dicho caracter (es decir, un bloque o array de pixels). Pero las fuentes pueden ser guardadas de diversas maneras dependiendo del uso que se le vaya a dar. En este capítulo abordaremos las fuentes desde diferentes puntos de vista, planteando las diversas aplicaciones que pueden llegar a tener.
Las fuentes multicolor, las monocolor y las monotextura son algunos de los puntos que vamos a tratar en este capítulo, nombrando sus aplicaciones, así como sus ventajas y desventajas.
char Letra[16*16];Aquí tenemos una función que nos dibuja un caracter de 16x16 guardado en un array lineal (o unidimensional), imprimiendo todos los pixels que lo forman de manera consecutiva:
void PutLetter (int x,int y) { int bx, by; for (by=0;by<16;by++) for (bx=0;bx<16;bx++ ) PutPixel(x+bx, y+by, Letra[(by*16)+bx]); }Este ejemplo nos muestra la manera en que una letra almacenada en una array lineal es imprimida en pantalla medinate la función PutPixel(). Pero si recordamos, las fuentes son un conjunto de letras (como la del listado) guardadas en un array. Ahora hemos de guardar el "set" completo de carácteres. De nuevo recurrimos a arrays bidimensionales:
char Fuente[16*16][256];En este array bidimensional guardamos espacio para 256 carácteres de 16x16 pixels. Como ya hemos comentado la preferencia de usar arrays unidimensionales, y sabiendo que cada letra ocupa 16*16=256 bytes( en nuestro ejemplo), si queremos almacenar n letras, necesitaremos (ancho*alto)*n bytes:
char Fuente[16*16*256];De tal modo que si queremos acceder al caracter 71 (letra G) siendo que por delante están almacenados los demás carácteres, habrá que hacer un cálculo que nos dé la dirección del comienzo de la letra 'G'. Y una vez obtenido el resultado del cálculo, podremos a partir de dicha dirección empezar a leer los datos del caracter 71. Si tenemos una fuente de 16x16 con 256 carácteres y queremos obtener la dirección de comienzo de la letra 'G' (caracter 71) se tendrá que hacer el siguiente cálculo:
offset = caracter * (ancho*alto);El listado 1 nos muestra como con un cálculo y dos bucles anidados es posible visualizar un caracter contenido en una fuente de letras en VideoRAM, así como su versión ensamblador optimizada en el listado 2.
LISTADO 1: Carácteres multicolor en C /*----------------------------------------------------- WriteChar(); Dibujo de caracteres multicolor en C. Pseudocódigo: OFFSET_CARACTER = caracter*(ancho*alto) BUCLE1 Desde 0 a ALTO BUCLE2 Desde 0 a ANCHO PutPixel(X+BUCLE2, Y+BUCLE1 , FUENTE[OFFSET]) END BUCLE2 END BUCLE1 --------------------------------------------------- */ void WriteChar ( int x, int y, char ncar, int ancho, int alto, char *fuente ) { int offset_car; char bx , by; offset_car = ncar*(ancho*alto); for (by = 0;by < alto;by ++) for (bx = 0;bx < ancho; bx++ ) PutPixel ( x+bx, y+by, fuente[offset+(by*ancho)+bx] ); }
LISTADO 2: Fuentes multicolor en ASM: *------------------------------------------ WriteChar(); Dibujo de caracteres en ASM. Pseudocódigo: DS:SI = OFFSET DEL CARACTER ES:DI = OFFSET DE LA MEMORIA DESTINO ES:DI = (y*320) + x BUCLE1 FOR 0 TO ALTO BUCLE2 FOR 0 TO ANCHO ES:[DI] = DS:[SI] DI++; SI++ END BUCLE2 DI += 320-ANCHO END BUCLE1 ------------------------------------------ */ void WriteChar ( int x, int y, char ncar, char masc, int ancho, int alto, char far *fuente, int segmento) { int offset_car; offset_car = ncar * (ancho*alto); asm { lds si, [fuente] /*DS:SI = fuente*/ add si, [offset_car] mov ax, [segmento] mov es, ax mov ax, [y] mov bx, ax shl ax, 8 shl bx, 6 add ax, bx add ax, [x] mov di, ax mov bx, [alto] } bucleY:; asm mov cx, [ancho] bucleX:; asm { mov al, ds:[si] cmp al, [masc] je NoPinta mov es:[di] , al } NoPinta: asm { inc di inc si dec cx jnz bucleX sub di, cx add di, 320 dec bx jnz bucleY } }Este estilo de fuentes tiene la ventaja de poder contener variedad de color en un mismo caracter, pero por eso mismo tienen el inconveniente de ocupar mucho tamaño. Existen otros métodos que nos ayudan a ahorrar espacio, pero que tienen unas desventajas que ya se comentarán más adelante.
En una letra monocolor de 8x16 (las más comunes) un carácter ocuparía 16 bytes debido a que cada color del carácter está guardado de forma simbólica en un bit, donde hay color si el bit está activado y no lo hay si el bit no lo está. Por lo tanto un byte puede guardar la información de los 8 colores que debe tener la letra con el estado de sus bits.
Byte 1: 00111100b Byte 2: 01100110b Byte 3: 11000011b Byte 4: 11000011b Byte 5: 11111111b Byte 6: 11111111b Byte 7: 11000011b ... ... Byte 16: 11000011bY en el array quedaría:
char Letra[16]={60,102,195,195,255,255,195,...,195};Sabiendo esto, ahora sólo hay que comprobar el estado de cada uno de los bits que contiene un byte con el fin de dibujar un pixel por cada bit que se encuentre activo. Esto debe hacerse con el cálculo del AND lógico de los operandos (& en lenguaje C), que nos da el resultado de 1 cuando dos bits se encuentran activos. De tal forma debemos calcular el AND entre un byte de la letra y el byte que contenga a 1 el bit que queramos comprobar. Así nos dará como resultado un valor mayor de 0 si el bit especificado está activo y 0 si no lo está.
11000011b AND 10000000b = 10000000b (activo) 11000011b AND 01000000b = 01000000b (activo) 11000011b AND 00100000b = 00000000b (no activo) ... (comprobar con todos los bits) ...Teniendo una fuente de 8x16, el código que visualiza un caracter contiene un sólo bucle que se repite 16 veces de alto que tiene el caracter (16 bytes), en cuyo interior desarrolla los cálculos que comprueban los bits del byte actual.
char Letra[16]; void PutChar (int x,int y,char color) { char valor; for (bucle=0;bucle<16;bucle++) { valor = Letra[bucle]; if ((valor & 128) != 0) PutPixel (x,y+bucle,color); if ((valor & 64) != 0) PutPixel (x,y+bucle,color); if ((valor & 32) != 0) PutPixel (x,y+bucle,color); if ((valor & 16) != 0) PutPixel (x,y+bucle,color); if ((valor & 8) != 0) PutPixel (x,y+bucle,color); if ((valor & 4) != 0) PutPixel (x,y+bucle,color); if ((valor & 2) != 0) PutPixel (x,y+bucle,color); if ((valor & 1) != 0) PutPixel (x,y+bucle,color); } }Este ejemplo visualiza en pantalla un sólo caracter guardado en memoria. Pero al tener un fuente con todos los carácteres, habrá que hacer el cálculo que realizábamos con las fuentes multicolor para desplazarnos al comienzo del caracter deseado (listado 3).
LISTADO 3: Trazado de un carácter monocolor. /*---------------------------------------------- PutChar8x16( ); Dibujo de letras monocolor. ----------------------------------------------- */ void PutChar8x16 (int x, int y, unsigned char color, unsigned char caracter,char *fuente) { int offset_inicio, by; unsigned char getbyte; offset_inicio = caracter * 16; for (by=0; by<16; by++ , offset_inicio++) { getbyte = fuente[offset_inicio]; if((getbyte & 128)!= 0) PutPixel( x , y + by, color); if((getbyte & 64)!= 0) PutPixel( x+1 , y + by, color); if((getbyte & 32)!= 0) PutPixel( x+2 , y + by, color); if((getbyte & 16)!= 0) PutPixel( x+3 , y + by, color); if((getbyte & 8)!= 0) PutPixel( x+4 , y + by, color); if((getbyte & 4)!= 0) PutPixel( x+5 , y + by, color); if((getbyte & 2)!= 0) PutPixel( x+6 , y + by, color); if((getbyte & 1)!= 0) PutPixel( x+7 , y + by, color); } }
Si recordamos, las letras multicolor guardaban el color en la misma fuente donde el color de un pixel lo contenia uno y cada uno de los bytes con los que eran formados los carácteres. Ahora las fuentes monotextura podrán tener aplicada una variedad de colores como las multicolor guardando un bloque de bytes (textura) en un array que contenga tantos bytes como de alto por ancho sean los carácteres de la fuente monocolor. Así se podrá asignar cada uno de los colores de la textura a los pixels que forman un caracter. Aquí se muestra la manera en que una letra monocolor es dibujada en pantalla aplicándosele una textura de color.
char Letra[16]; char Textura[8*16]; void PrintLetter ( int x , int y ) { char offset_letra, offs_text, valor; offset_letra = offs_text = 0; for(bucle=0;bucle < 16;bucle++,offset_letra++) { valor = Letra[offset_letra]; if ((valor & 128) !=0) PutPixel(x,y+bucle,Textura[offs_text]); offs_text++; (...resto de condiciones...) if ((valor & 1) !=0) PutPixel(x+7,y+bucle,Textura[offs_text]); offs_text++; } }Esta función nos da ejemplo de cómo se puede aplicar una textura guardada en un array a un caracter de una fuente monocolor. Pero si disponemos de un estilo de fuente monocolor y un array que contenga una textura, podremos ser capaces mediante una función (listado 4) de dibujar en pantalla cualquier caracter de la fuente. Este método puede ser muy útil y sencillo de manejar para simples programas que no requieran mucho espacio y sin embargo tengan una buena presentación en cuanto a carácteres.
LISTADO 4: Letras texturadas. /*------------------------------------------------- PutChar_MonoTextura8x16( ); Dibujo de letras monocolor con textura --------------------------------------------------- */ void PutChar_MonoTextura8x16 (int x, int y, unsigned char caracter,char *fuente, char *textura ) { int offset_inicio, by; unsigned char getbyte; char offset_textura; offset_inicio = caracter * 16; offset_textura = 0; for (by=0; by<16; by++, offset_inicio++) { getbyte = fuente[offset_inicio]; if((getbyte & 128)!=0) PutPixel( x, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 64)!=0) PutPixel( x+1, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 32)!=0) PutPixel( x+2, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 16)!=0) PutPixel( x+3, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 8)!=0) PutPixel( x+4, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 4)!=0) PutPixel( x+5, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 2)!=0) PutPixel( x+6, y+by, textura[offset_textura]); offset_textura++; if((getbyte & 1)!=0) PutPixel( x+7, y+by, textura[offset_textura]); offset_textura++; } }
El centrado de texto se consigue con una simple operación que calcula la coordenada X a partir de la cual el texto debe ser visualizado para estar centrado en un punto. Supongamos que quiero calcular la coordenada X de un texto que tiene n pixels de largo contando los espacios entre carácteres, y queriéndolo centrar en el punto (100,50). El cálculo sería de la siguiente forma:
X_INICIAL = 100 - (n/2)Otro nuevo efecto sería dotar a un texto de sombra, produciendo un efecto de relieve que da una imagen diferente de la habitual. Dar sombra a un texto no es nada complicado ya que se logra dibujando dos veces la cadena en diferentes posiciones. Teniendo como coordenadas los valores X e Y, la cadena que representa la sombra se dibujará en X+1 e Y+1, y la misma cadena se dibuja a continuación en las coordenadas X e Y. De tal forma el texto queda escrito sobre la sombra que se encuentra en una posición más hacia la derecha y hacia abajo (aunque también podemos dibujar más alejada o hacia arriba, pero el efecto más visual se consigue con (+1,+1)).
LISTADO 5: Trazado de fuentes con sombra: /*---------------------------------------------------------- Print8x16( ); Imprime una cadena de texto con sombra ----------------------------------------------------------- */ void Print8x16 (int x ,int y ,unsigned char color, unsigned char colsombra ,char *frase,char *fuente ) { int loop = 0; while (frase[loop] != 0) { PutChar8x16 ( x+1 , y+1 , colsombra , frase[loop] , fuente ); PutChar8x16 ( x , y , color , frase[loop] , fuente ); x = x + 8 + 1; /* separación entre carácteres */ loop++; } }Para invertir un carácter tan sólo hemos de dibujar los pixels que lo componen en sentido inverso. En el caso de las fuentes monocolor es preciso comprobar los bits de todos los bytes, pero si antes lo hacíamos analizando del último bit al primero, ahora podemos hacerlos de forma invertida. Por tanto el resultado de dibujar la letra 'F' invertida quedaría así:
XXXXXXXX XXXXXXXX .....XXX ..XXXXXX ..XXXXXX .....XXX .....XXX .....XXXEstos son algunos de los efectos que pueden hacerse con las fuentes de texto, pero todavía quedan muchos que deben salir de la propia imaginación del programador, como letras en cursiva al incrementar la x de dibujo en cada pixel, aplicación de la función seno durante el dibujo, escalación, etc.
Mediante la subfunción 1100h de la INT 10h es posible cambiar la dirección del nuevo buffer de carácteres de 8x16. De esta forma el MS-DOS puede adquirir una nueva imagen distinta de la habitual (como realiza el programa FDOS incluido en el directorio UTILS).
La figura 2 ilustra como es guardado el mismo caracter de una forma y de otra. De modo que si, por ejemplo, deseamos realizar un scroll de texto en modo gráfico, donde los carácteres tienen que salir por el borde de la pantalla visualizando líneas verticales, será más cómodo y rápido el guardar las fuentes en el orden en el que salen.
Para demostrar las posibilidades de las fuentes de texto y como ejemplo de almacenaje de las fuentes en sentido vertical disponemos de 2 ejemplos en el CD que acompaña a la revista que realizan un scroll horizontal por software, tan utilizado en el mundo de las demos.
Pulse aquí para bajarse los ejemplos y listados del artículo (66 Kb).
Figura 1: "Definición de carácteres
individuales."
Figura 2: "Métodos de almacenaje de las
fuentes."
Santiago Romero