Simple8

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 64PC Engine
Pixels affichés320 × 200256 × 224
Grille de caractères40 × 2532 × 28 visibles
Une case contientle numéro du caractère, et sa couleurle numéro de la tuile, et le numéro de sa palette
Mémoire d'un écran de texte1 000 octets, plus 1 000 couleurs2 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.

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)
Composantesde 0 à 7de 0 à 255, comme dans un logiciel de dessin
Sur PC Enginela couleur exacteconvertie à la couleur la plus proche
Sur C64le 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 écritCommodore 64PC 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 64PC Engine
setFontColor(n)n est une couleur, de 0 à 15n est une palette, de 0 à 15
La lettre s'affiche avecla couleur nla couleur 1 de la palette n
Couleurs de texte possibles16, sans rien préparer16, 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
Écran du Commodore 64 : NORMAL, INVERSE en inversion vidéo, deux lignes, et un texte coupé dans une zone
Inversion, passage à la ligne et zone de texte.

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]);
        }
    }
}
Écran du Commodore 64 : les 16 couleurs, chacune avec un pavé, son numéro et son nom
Les 16 couleurs du Commodore 64.

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 :

bits876543210
rôlevertrougebleu
gris 5101101101
Écran de la PC Engine : deux dégradés de 8 pavés, du noir au rouge vif et du noir au blanc
Deux dégradés calculés sur PC Engine.

À retenir

À vous de jouer

  1. 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.

    Le nuancier du C64 sur fond bleu : le pavé noir se voit, le pavé bleu a disparu
    Sur fond bleu, le noir apparaît et le bleu disparaît.
  2. 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é v est donc simplement v, 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
            }
    Deux dégradés de 8 pavés sur PC Engine : rouge, puis jaune
    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.

  3. É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 \n ramènerait la deuxième ligne en colonne 0. Pour aligner les trois lignes, on appelle donc setCursor avant 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.

  4. 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 cadre le 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('*');
            }
        }
    }
    Commodore 64 : un cadre d'étoiles bleu clair au centre de l'écran, le texte blanc à l'intérieur
    Le texte va à la ligne au bord de la zone, au milieu d'un mot s'il le faut.

    Sur PC Engine

    La fonction cadre est la même, avec vdc à la place de vic. 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();
        }
    PC Engine : le même cadre d'étoiles, plus étroit, au centre de l'écran
    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.