Série 9 – Art ASCII

Introduction

Le but de cette série est d'utiliser l'approche compositionnelle décrite au cours pour écrire une petite bibliothèque permettant de dessiner des images à base de caractères, ce que l'on nomme parfois « art ASCII » (ASCII art).

Pour simplifier les choses, les images de cette bibliothèque sont rectangulaires et décrites par l'interface ci-dessous :

public interface ASCIImage {
    public int width();
    public int height();
    public List<String> drawing();

    default public void printOn(PrintStream s) {
        drawing().stream().forEach(s::println);
    }
}

Les méthodes width et height donnent la largeur et la hauteur de l'image, en caractères, et drawing donne le contenu de l'image sous la forme d'une liste immuable de chaînes qui sont les lignes de l'image. Cette liste doit avoir un nombre d'éléments égal à la hauteur de l'image, et chacun de ces éléments doit être une chaîne de longueur égale à la largeur de l'image.

Par exemple, l'image ci-dessous :

baba
baba
baba

a une largeur de 4 (caractères), une hauteur de 3 (lignes) et son dessin est une liste contenant trois fois la chaîne baba.

La méthode par défaut printOn permet d'imprimer l'image à laquelle on l'applique sur un flot de sortie de type PrintStream. Par exemple, pour dessiner une image sur la console, il suffit d'appeler cette méthode en lui passant System.out en argument.

Pour démarrer cette série, nous mettons à votre disposition une archive Zip contenant l'interface ASCIImage ci-dessus ainsi qu'une classe utilitaire nommée Strings contenant des méthodes de manipulation de chaînes de caractères. Avant d'aller plus loin, importez cette archive dans votre projet puis familiarisez-vous avec le code fourni.

Exercice 1 : images de base

Comme toujours lorsqu'on utilise l'approche compositionnelle, il convient de définir des moyens d'obtenir des valeurs (ici des images ASCII) de base, c-à-d qui ne sont pas dérivées de valeurs (images ASCII) existantes.

Il vous est demandé de définir les deux types d'images ASCII de base suivants :

  1. une image obtenue à partir d'une chaîne de caractères, dont la largeur est égale à la longueur de la chaîne et la hauteur est 1,
  2. une image de largeur et de hauteur donnée, composée uniquement d'un caractère donné qui remplit tout l'image.

Bien entendu, à chacun de ces types d'images de base correspond une classe implémentant l'interface ASCIImage et donnant une définition appropriée de ses trois méthodes abstraites. Notez au passage que la première de ces deux classes peut être vue comme un adaptateur qui adapte une chaîne de caractères pour en faire une image ASCII.

Une fois ces deux classes définies, ajoutez deux méthodes statiques dans l'interface ASCIImage simplifiant la création de leurs instances. Par exemple, pour créer une image à partir d'une chaîne de caractères, ajoutez une méthode statique nommée fromString qui, étant donnée une chaîne de caractères, retourne une image dont le dessin est la chaîne en question. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 20x1 :
// La malade pédala mal
ASCIImage.fromString("La malade pédala mal");

// Permet d'obtenir l'image 3x2 :
// ***
// ***
ASCIImage.filled(3, 2, '*');

Exercice 2 : transformations

Les images de base étant définies, il est temps de passer aux transformations, qui permettent d'obtenir une nouvelle image à partir d'une image existante.

Il vous est demandé de définir les deux transformations suivantes :

  1. une transformation permettant de faire une symétrie horizontale d'une image,
  2. une transformation permettant de transposer une image, c-à-d d'inverser le rôle de ses lignes et de ses colonnes, exactement comme lors de la transposition d'une matrice.

Bien entendu, les deux classes définissant ces transformations sont des décorateurs.

Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface ASCIIImage simplifiant la création de leurs instances. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 20x1 :
// lam aladép edalam aL
ASCIImage.fromString("La malade pédala mal")
    .flippedHorizontally();

// Permet d'obtenir l'image 1x3 :
// é
// t
// é
ASCIImage.fromString("été").transposed();

Exercice 3 : compositions

Après avoir défini les transformations, on passe aux compositions, dont le but est d'obtenir une nouvelle image à partir de plusieurs images existantes.

Il vous est demandé de définir les deux compositions suivantes :

  1. la composition « côte à côte » qui compose deux images en plaçant la première à gauche de la seconde ; les deux images peuvent avoir une hauteur différente, auquel cas des espaces sont insérées en bas de l'image la mois haute,
  2. la composition « l'un sur l'autre » qui compose deux images en plaçant la première au dessus de la seconde (verticalement) ; les deux images peuvent avoir une largeur différente, auquel cas des espaces sont ajoutées à droite de l'image la moins large.

Bien entendu, les deux classes définissant ces compositions sont des composites.

Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface ASCIImage simplifiant la création de leurs instances. Ces méthodes devraient pouvoir s'utiliser ainsi :

// Permet d'obtenir l'image 18x2 :
// Un rectangle : ###
//                ###
ASCIImage.fromString("Un rectangle : ")
    .leftOf(ASCIImage.filled(3, 2, '#'));

// Permet d'obtenir l'image 4x3 :
// XXX 
// OOOO
// OOOO
ASCIImage.filled(3, 1, 'X')
    .above(ASCIImage.filled(4, 2, 'O'));

Exercice 4 : dessin d'un échiquier

Utilisez aussi judicieusement que possible les méthodes ajoutées à l'interface ASCIImage pour définir l'image d'un échiquier ci-dessous :

+------------------------+
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
|###   ###   ###   ###   |
|###   ###   ###   ###   |
|   ###   ###   ###   ###|
|   ###   ###   ###   ###|
+------------------------+

Pour dessiner le cadre autour de cet échiquier, ajoutez encore une méthode de transformation à l'interface ASCIImage, nommée p.ex. framed et permettant d'ajouter un cadre autour de l'image à laquelle on l'applique. Contrairement aux autres méthodes, celle-ci s'exprime assez facilement en terme de méthodes déjà existantes, et il n'est donc pas nécessaire de définir une classe pour les images encadrées.

Exercice 5 : améliorations libres

Si vous avez terminé les exercices ci-dessus avant la fin de la séance d'exercices, ajoutez d'autres types d'images de base, transformations ou compositions qui vous semblent intéressantes : translations, superpositions, etc.