Couleurs et affichage
Depuis le chapitre 2, nos programmes écrivent du texte en couleur. Ce chapitre regarde de plus près comment une machine rétro fabrique son image : la grille de caractères, les couleurs qu'elle sait afficher, et les moyens de mettre un texte en forme.
Comment une machine rétro affiche
Un écran est fait de pixels, de petits points. Celui du C64 en compte 320 × 200, soit 64 000. Décrire chacun d'eux demanderait des milliers d'octets, et ces machines n'en avaient pas tant à consacrer à l'image.
Elles s'y prennent autrement : l'écran est une mosaïque de petits carrés
de 8 × 8 pixels. Ces carrés sont dessinés une fois pour toutes (les lettres de la police,
par exemple), et l'écran ne retient, pour chaque case, que le numéro du
carré à afficher. C'est ce qu'on appelle le mode texte, ou le mode
tuiles : print ne dessine pas de pixels, il range des numéros de
caractères dans les cases de cette grille.
| Commodore 64 | PC Engine | |
|---|---|---|
| Pixels affichés | 320 × 200 | 256 × 224 |
| Grille de caractères | 40 × 25 | 32 × 28 visibles |
| Une case contient | le numéro du caractère, et sa couleur | le numéro de la tuile, et le numéro de sa palette |
| Mémoire d'un écran de texte | 1 000 octets, plus 1 000 couleurs | 2 octets par case |
Sur C64, une image dessinée pixel par pixel, en mode bitmap, prend 8 000 octets contre 1 000 en mode texte. D'où l'importance du mode texte, même pour les jeux : leurs décors sont faits de caractères redessinés.
Les couleurs : fixes ou programmables
Chaque machine a sa propre façon de gérer les couleurs. Nos deux machines représentent les deux grandes familles.
Le C64 : 16 couleurs fixes
Le C64 connaît 16 couleurs, toujours les mêmes, numérotées de 0 à 15.
On ne peut pas en changer la teinte : on choisit simplement un numéro. La bibliothèque
vic.base leur donne des noms :
vic.setBackgroundColor(vic.BLUE); // plutôt que setBackgroundColor(6)
vic.setFontColor(vic.YELLOW); // plutôt que setFontColor(7)
Les seize sont visibles dans le nuancier, à la fin de ce chapitre.
La PC Engine : des palettes à remplir
La PC Engine peut afficher 512 couleurs différentes : chacune mélange du rouge, du vert et du bleu, chaque composante allant de 0 à 7 (8 × 8 × 8 = 512). Mais elle ne les affiche pas toutes à la fois : les couleurs sont rangées dans des palettes, que l'on remplit soi-même.
- Il y a 32 palettes de 16 couleurs : 16 pour les tuiles (numéros 0 à 15) et 16 pour les sprites (16 à 31).
- Chaque case de l'écran choisit une palette. Une même tuile peut donc s'afficher en rouge à un endroit et en bleu à un autre.
- La couleur 0 de la palette 0 est celle du fond. Dans les autres palettes, la couleur 0 ne s'affiche pas : elle laisse voir le fond.
vce.setPalette(2, 1, RGB(7, 6, 1)); // la couleur 1 de la palette 2 : un jaune
Écrire une couleur : RGB() et COLOR()
Simple8 propose deux façons d'écrire une couleur à partir de ses composantes rouge, vert et bleu :
RGB(r, g, b) | COLOR(r, g, b) | |
|---|---|---|
| Composantes | de 0 à 7 | de 0 à 255, comme dans un logiciel de dessin |
| Sur PC Engine | la couleur exacte | convertie à la couleur la plus proche |
| Sur C64 | le numéro de la couleur fixe la plus proche | |
COLOR() est la plus portable : la même ligne donne, sur
chaque machine, la couleur la plus proche de celle demandée. Valeurs mesurées sur les deux
machines :
| On écrit | Commodore 64 | PC Engine |
|---|---|---|
COLOR(255, 0, 0) | 2 (rouge) | $38 |
COLOR(0, 0, 255) | 6 (bleu) | $07 |
COLOR(128, 128, 128) | 12 (gris) | $DB |
COLOR(255, 128, 0) | 8 (orange) | $F8 |
RGB(7, 0, 0) | 2 (rouge) | $38 |
Calculées à la compilation. RGB() et COLOR()
sont remplacées par leur valeur au moment de compiler. Leurs arguments doivent donc être
des nombres écrits dans le code. Avec une variable, RGB(v, 0, 0) ne
fonctionne pas : pour l'instant, le compilateur ne le signale pas, et la couleur vaut 0,
le noir. L'exemple PC Engine de ce chapitre montre comment calculer une couleur pendant
l'exécution.
La couleur du texte
setFontColor choisit la couleur des caractères écrits ensuite. Son argument
n'a pas le même sens sur les deux machines, parce que les cases de l'écran ne retiennent
pas la même chose :
| Commodore 64 | PC Engine | |
|---|---|---|
setFontColor(n) | n est une couleur, de 0 à 15 | n est une palette, de 0 à 15 |
| La lettre s'affiche avec | la couleur n | la couleur 1 de la palette n |
| Couleurs de texte possibles | 16, sans rien préparer | 16, en remplissant la couleur 1 de 16 palettes |
// C64 : une ligne en jaune
vic.setFontColor(vic.YELLOW);
vic.print("ATTENTION");
// PC Engine : la palette 3 reçoit un jaune, puis le texte l'utilise
vce.setPalette(3, 1, RGB(7, 7, 2));
vdc.setFontColor(3);
vdc.print("ATTENTION");
Mettre le texte en forme
Les bibliothèques de texte offrent, sur les deux machines, les mêmes moyens de mise en forme.
vic.setCursor(2, 2);
vic.print("NORMAL ");
vic.setInvertOn();
vic.print("INVERSE"); // fond et lettre échangés
vic.setInvertOff();
vic.setCursor(2, 4);
vic.print("LIGNE 1\nLIGNE 2"); // \n : à la ligne
vic.setTextArea(20, 8, 10, 4); // une zone de 10 colonnes sur 4 lignes
vic.setFontColor(vic.YELLOW);
vic.print("UN TEXTE TROP LONG POUR LA ZONE");
vic.clearTextArea(); // le texte reprend tout l'écran
L'inversion vidéo
Entre setInvertOn() et setInvertOff(), chaque caractère
s'affiche en négatif : la lettre prend la couleur du fond, et le reste de la case la
couleur du texte. Une espace inversée donne ainsi un pavé plein de la
couleur du texte : c'est ce qui dessine les couleurs du nuancier, plus bas.
Sur PC Engine, l'inversion utilise les caractères 128 à 255 de la police : la police fournie doit les contenir. C'est le cas de celle des exemples.
Passer à la ligne
Dans un texte, \n fait passer à la ligne suivante. Attention : la nouvelle
ligne repart de la colonne 0, et non de la colonne choisie par
setCursor. Pour aligner plusieurs lignes, on place le curseur avant chacune
d'elles.
Une zone de texte
setTextArea(x, y, largeur, hauteur) limite le texte à un rectangle, une
fenêtre, et y place le curseur. Arrivé au bord de la zone, le texte continue à la
ligne suivante de la zone, au milieu d'un mot s'il le faut. clearTextArea()
rend tout l'écran au texte.
Où est le curseur ?
getCursorX() et getCursorY() donnent la position du curseur :
utile pour reprendre l'écriture là où elle s'était arrêtée, ou pour centrer un texte.
Deux exemples, deux façons de faire
Les deux machines gèrent leurs couleurs si différemment que chacune a ici son propre exemple.
Sur Commodore 64 : le nuancier
Ce programme affiche les 16 couleurs du C64 : pour chacune, un pavé de couleur, son numéro et son nom.
// Couleurs et affichage : le nuancier des 16 couleurs (Commodore 64)
architecture commodore.c64;
addlib vic.base;
addlib vic.screentext;
unit {
const string NOMS[] = {
"NOIR", "BLANC", "ROUGE", "CYAN", "VIOLET", "VERT", "BLEU", "JAUNE",
"ORANGE", "MARRON", "ROSE", "GRIS FONCE", "GRIS", "VERT CLAIR", "BLEU CLAIR", "GRIS CLAIR"
};
main() {
vic.initScreen();
vic.setBorderColor(vic.BLACK);
vic.setBackgroundColor(vic.BLACK);
vic.clearScreen();
vic.setFontColor(vic.WHITE);
vic.setCursor(8, 1);
vic.print("LES 16 COULEURS DU C64");
// deux colonnes de 8 couleurs
for (ubyte c = 0; c < 16; c++) {
ubyte x = 2;
if (c >= 8) {
x = 21;
}
vic.setCursor(x, (ubyte)(4 + (c % 8) * 2));
// un pavé de la couleur : des espaces en inversion vidéo
vic.setFontColor(c);
vic.setInvertOn();
vic.print(" ");
vic.setInvertOff();
// son numéro et son nom, en blanc
vic.setFontColor(vic.WHITE);
vic.putChar(' ');
if (c >= 10) {
vic.putChar('1');
}
vic.putChar((ubyte)('0' + c % 10));
vic.putChar(' ');
vic.print(@.NOMS[c]);
}
}
}
- Le numéro de la couleur,
c, sert à la fois d'argument àsetFontColoret d'indice dans le tableau des noms. - Le pavé est fait de trois espaces en inversion vidéo.
c % 8donne la ligne dans la colonne : les couleurs 0 à 7 à gauche, 8 à 15 à droite.- Le noir, couleur 0, ne se voit pas : il est de la couleur du fond.
Sur PC Engine : fabriquer ses couleurs
La PC Engine n'a pas de couleurs toutes faites : ce programme les calcule. Il remplit 16 palettes avec deux dégradés de 8 nuances, un rouge et un gris, puis les affiche.
// Couleurs et affichage : fabriquer ses couleurs (PC Engine)
architecture nec.pce;
addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;
unit {
const int font = addResource("font.bin");
main() {
int source;
int length;
vdc.setBATSize(vdc.BAT_32x32);
vdc.initScreen();
resource.mapToMPR2(.font);
source = 0x4000 + resource.getOffset(.font);
length = resource.getLength(.font);
vdc.writeToVRAM(source, 0x1000, length);
vce.setPalette(0, 0, RGB(0, 0, 2)); // le fond, bleu nuit
// 16 palettes, 16 couleurs calculées : une couleur PC Engine s'écrit
// vert << 6 | rouge << 3 | bleu, chaque composante de 0 à 7
for (ubyte v = 0; v < 8; v++) {
vce.setPalette(v, 1, (uint)(v << 3)); // rouge v
vce.setPalette((ubyte)(8 + v), 1, (uint)((v << 6) | (v << 3) | v)); // gris v
}
vdc.clearScreen();
vdc.setFontColor(15); // la palette 15 : gris 7, le blanc
vdc.setCursor(5, 3);
vdc.print("FABRIQUER SES COULEURS");
dessinerDegrade(0, 8, "ROUGE");
dessinerDegrade(8, 15, "GRIS");
}
// 8 pavés, des palettes premiere à premiere + 7, à la ligne y
void dessinerDegrade(ubyte premiere, ubyte y, uint nom) {
vdc.setFontColor(15);
vdc.setCursor(4, (ubyte)(y - 2));
vdc.print(nom);
for (ubyte v = 0; v < 8; v++) {
vdc.setCursor((ubyte)(4 + v * 3), y);
vdc.setFontColor((ubyte)(premiere + v));
vdc.setInvertOn();
vdc.print(" ");
vdc.setInvertOff();
vdc.setFontColor(15);
vdc.setCursor((ubyte)(4 + v * 3), (ubyte)(y + 1));
vdc.putChar((ubyte)('0' + v));
}
}
}
Une couleur PC Engine est un nombre de 9 bits : 3 bits de vert, 3 de rouge, 3 de bleu. On la fabrique avec les opérations bit à bit du chapitre 3 :
| bits | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|
| rôle | vert | rouge | bleu | ||||||
| gris 5 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
v << 3placevdans les bits du rouge : un rouge d'intensitév.(v << 6) | (v << 3) | vmet la même valeur dans les trois composantes : un gris. Pourv= 7, c'est le blanc.- Calculée ainsi, la couleur peut dépendre d'une variable, ce que
RGB()ne permet pas.
À retenir
- Un écran rétro est une grille de caractères de 8 × 8 pixels : chaque case retient un numéro de caractère, et sa couleur ou sa palette.
- Le C64 a 16 couleurs fixes ; la PC Engine en a 512, rangées dans des palettes de 16 que l'on remplit.
RGB()écrit une couleur avec des composantes de 0 à 7,COLOR()de 0 à 255, et convient à toutes les machines ; les deux sont calculées à la compilation.setFontColor(n): une couleur sur C64, une palette sur PC Engine.- Inversion vidéo, passage à la ligne et zone de texte existent sur les deux machines.
À vous de jouer
- Dans le nuancier, changez le fond pour que le noir se voie. Quelle couleur disparaît
alors ?
Voir la solutionMasquer la solution
Il suffit de changer la couleur du fond (et, pour l'harmonie, celle de la bordure). Choisissons le bleu,
vic.BLUE: le pavé noir de la couleur 0 apparaît alors.main() { vic.initScreen(); vic.setBorderColor(vic.BLUE); vic.setBackgroundColor(vic.BLUE); // fond bleu : le noir se voit vic.clearScreen();Mais c'est maintenant le bleu, la couleur 6, qui disparaît : son pavé est exactement de la couleur du fond. Seuls son numéro et son nom, écrits en blanc, restent visibles. Quel que soit le fond choisi, une couleur disparaît toujours : le fond est forcément l'une des 16 couleurs, et le pavé qui porte le même numéro se confond avec lui. Pour voir les 16 pavés à la fois, il faudrait entourer chacun d'un cadre d'une autre couleur.

Sur fond bleu, le noir apparaît et le bleu disparaît. - Sur PC Engine, remplacez le dégradé de gris par un dégradé de bleu, puis de jaune
(rouge et vert ensemble). Pourquoi ne peut-on pas afficher un troisième dégradé de 8
nuances ?
Voir la solutionMasquer la solution
Tout se joue dans la ligne qui calcule le second dégradé. Une couleur PC Engine range le vert dans les bits 6 à 8, le rouge dans les bits 3 à 5 et le bleu dans les bits 0 à 2. Un bleu d'intensité
vest donc simplementv, sans décalage. Un jaune est un mélange de rouge et de vert à égalité :(v << 6) | (v << 3).Le dégradé de bleu
vce.setPalette(0, 0, RGB(0, 0, 0)); // le fond, noir : un fond bleu cacherait le bleu 2 // 16 palettes, 16 couleurs calculées : une couleur PC Engine s'écrit // vert << 6 | rouge << 3 | bleu, chaque composante de 0 à 7 for (ubyte v = 0; v < 8; v++) { vce.setPalette(v, 1, (uint)(v << 3)); // rouge v vce.setPalette((ubyte)(8 + v), 1, (uint)v); // bleu v }Le fond a dû changer lui aussi : le bleu nuit de l'exemple,
RGB(0, 0, 2), est exactement le bleu 2 du dégradé, dont le pavé aurait disparu (c'est l'exercice précédent, version PC Engine). Sur fond noir, on voit les nuances 1 à 7 ; la nuance 0, noire, se confond avec le fond, comme le rouge 0.Le dégradé de jaune
for (ubyte v = 0; v < 8; v++) { vce.setPalette(v, 1, (uint)(v << 3)); // rouge v vce.setPalette((ubyte)(8 + v), 1, (uint)((v << 6) | (v << 3))); // jaune v : vert v + rouge v }
Le gris remplacé par un dégradé de jaune. Pourquoi pas un troisième dégradé ? La police n'utilise que la couleur 1 de sa palette : chaque palette ne donne donc qu'une seule couleur de texte, et chaque pavé coûte une palette entière. Les tuiles n'ont que 16 palettes (0 à 15), et les deux dégradés de 8 nuances les prennent toutes. Les palettes 16 à 31 existent, mais elles sont réservées aux sprites : une case de l'écran ne peut pas les choisir. Remarquez d'ailleurs que le titre, écrit avec la palette 15, n'est plus blanc : il prend la couleur de la dernière nuance (bleu 7 ou jaune 7), car il ne reste aucune palette libre pour un vrai blanc. Dans l'exemple du chapitre, le gris 7 était justement le blanc.
- Écrivez un message de trois lignes alignées en colonne 10, en plaçant le curseur avant
chaque ligne.
Voir la solutionMasquer la solution
Un
\nramènerait la deuxième ligne en colonne 0. Pour aligner les trois lignes, on appelle doncsetCursoravant chacune d'elles, avec la même colonne, 10, et une ligne de plus à chaque fois.Sur Commodore 64
// Trois lignes alignées en colonne 10 (Commodore 64) architecture commodore.c64; addlib vic.base; addlib vic.screentext; unit { main() { vic.initScreen(); vic.setBorderColor(vic.BLACK); vic.setBackgroundColor(vic.BLACK); vic.clearScreen(); vic.setFontColor(vic.WHITE); // un \n repartirait en colonne 0 : on place le curseur avant chaque ligne vic.setCursor(10, 5); vic.print("PREMIERE LIGNE"); vic.setCursor(10, 6); vic.print("DEUXIEME LIGNE"); vic.setCursor(10, 7); vic.print("TROISIEME LIGNE"); } }Sur PC Engine
Après la préparation habituelle (police copiée en mémoire vidéo, palette 0 en blanc), le message s'écrit de la même façon :
vdc.clearScreen(); vdc.setFontColor(0); // un \n repartirait en colonne 0 : on place le curseur avant chaque ligne vdc.setCursor(10, 5); vdc.print("PREMIERE LIGNE"); vdc.setCursor(10, 6); vdc.print("DEUXIEME LIGNE"); vdc.setCursor(10, 7); vdc.print("TROISIEME LIGNE"); } }Les trois lignes commencent bien en colonne 10 sur les deux machines. Sur PC Engine, l'écran n'a que 32 colonnes : la plus longue ligne, « TROISIEME LIGNE », va jusqu'à la colonne 24, et tient donc encore.
- Placez une zone de texte au centre de l'écran, entourée d'un cadre d'étoiles.
Voir la solutionMasquer la solution
Il faut d'abord choisir la zone. Sur C64, l'écran fait 40 × 25 cases : une zone de 20 colonnes sur 6 lignes est centrée si elle commence en colonne (40 − 20) / 2 = 10, et vers la ligne 9. Sur PC Engine (32 × 28 cases visibles), une zone de 16 colonnes commence en colonne 8, et une zone de 6 lignes vers la ligne 11.
Le cadre se dessine autour de la zone, une case plus loin de chaque côté : sinon le texte écrirait par-dessus les étoiles. Une fonction
cadrele trace : deux lignes d'étoiles en haut et en bas, puis une étoile à gauche et une à droite de chaque ligne de la zone. Elle reçoit la zone elle-même, ce qui évite de recalculer les positions à la main.Sur Commodore 64
// Une zone de texte au centre, dans un cadre d'étoiles (Commodore 64) architecture commodore.c64; addlib vic.base; addlib vic.screentext; unit { main() { vic.initScreen(); vic.setBorderColor(vic.BLACK); vic.setBackgroundColor(vic.BLACK); vic.clearScreen(); // la zone : 20 colonnes sur 6 lignes, centrée dans 40 x 25 vic.setFontColor(vic.LIGHT_BLUE); cadre(10, 9, 20, 6); vic.setTextArea(10, 9, 20, 6); vic.setFontColor(vic.WHITE); vic.print("CE TEXTE RESTE DANS SA ZONE, AU CENTRE DE L'ECRAN."); vic.clearTextArea(); } // un cadre d'étoiles autour de la zone (x, y, largeur, hauteur) void cadre(ubyte x, ubyte y, ubyte largeur, ubyte hauteur) { // le haut et le bas, une case plus large de chaque côté vic.setCursor((ubyte)(x - 1), (ubyte)(y - 1)); for (ubyte i = 0; i < largeur + 2; i++) { vic.putChar('*'); } vic.setCursor((ubyte)(x - 1), (ubyte)(y + hauteur)); for (ubyte i = 0; i < largeur + 2; i++) { vic.putChar('*'); } // les côtés : une étoile à gauche et une à droite sur chaque ligne for (ubyte j = 0; j < hauteur; j++) { vic.setCursor((ubyte)(x - 1), (ubyte)(y + j)); vic.putChar('*'); vic.setCursor((ubyte)(x + largeur), (ubyte)(y + j)); vic.putChar('*'); } } }
Le texte va à la ligne au bord de la zone, au milieu d'un mot s'il le faut. Sur PC Engine
La fonction
cadreest la même, avecvdcà la place devic. Seuls changent les couleurs, préparées dans deux palettes, et la position de la zone :vce.setPalette(0, 0, RGB(0, 0, 0)); vce.setPalette(0, 1, RGB(7, 7, 7)); // palette 0 : le texte, blanc vce.setPalette(1, 1, RGB(3, 5, 7)); // palette 1 : le cadre, bleu clair vdc.clearScreen(); // la zone : 16 colonnes sur 6 lignes, centrée dans 32 x 28 vdc.setFontColor(1); cadre(8, 11, 16, 6); vdc.setTextArea(8, 11, 16, 6); vdc.setFontColor(0); vdc.print("CE TEXTE RESTE DANS SA ZONE, AU CENTRE DE L'ECRAN."); vdc.clearTextArea(); }
La même zone sur PC Engine, sur 16 colonnes. Le texte ne déborde jamais du cadre : arrivé au bord de la zone, il continue à la ligne suivante de la zone. Il coupe les mots (« D » et « E » sur C64, « DA » et « NS » sur PC Engine), comme le chapitre l'annonçait. Pour l'éviter, il faudrait découper le texte soi-même, ligne par ligne.
