Art ASCII
CS-108 — Série 6
Introduction
Cette série a pour but de mettre en œuvre une bibliothèque simple de dessin d'images constituées de caractères, ce que l'on nomme parfois « art ASCII » (ASCII art), en utilisant l'approche algébrique décrite en cours et basée sur les patrons Decorator et Composite.
Pour simplifier les choses, les images de cette bibliothèque sont rectangulaires et décrites par l'interface ci-dessous :
public interface TextImage { int width(); int height(); List<String> drawing(); default void printOn(PrintStream stream) { for (String s : drawing()) stream.println(s); } }
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 TextImage
ci-dessus. Avant d'aller plus loin, créez un projet à partir du contenu de cette archive.
Exercice 1 : images de base
Avant de pouvoir définir des décorateurs, il faut bien entendu définir quelques images de base qui pourront ensuite être décorées.
Il vous est demandé de définir les deux types d'images textuelles de base suivants :
- 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,
- une image de largeur et de hauteur données, 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 TextImage
et donnant une définition appropriée de ses trois méthodes abstraites.
Avant de vous lancer dans la programmation de ces classes, regardez bien les méthodes offertes par String
et Collections
, car certaines d'entre elles peuvent grandement faciliter votre travail — p.ex. nCopies
de Collections
, ou repeat
de String
.
Une fois ces deux classes définies, ajoutez deux méthodes statiques dans l'interface TextImage
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 TextImage.fromString("La malade pédala mal"); // Permet d'obtenir l'image 3x2 : // *** // *** TextImage.filled(3, 2, '*');
Exercice 2 : décorateurs
Les images de base étant définies, il est temps de passer aux décorateurs, qui permettent d'obtenir une nouvelle image par transformation d'une image existante.
Il vous est demandé de définir les deux décorateurs suivants :
- un décorateur permettant de faire une symétrie horizontale d'une image,
- un décorateur 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.
Notez que la méthode reverse
de StringBuilder
peut vous être fort utile lors de la programmation de l'un de ces décorateurs.
Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface TextImage
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 TextImage.fromString("La malade pédala mal") .flippedHorizontally(); // Permet d'obtenir l'image 1x3 : // é // t // é TextImage.fromString("été").transposed();
Exercice 3 : composites
En plus des décorateurs, qui permettent d'appliquer une transformation aux images, il est intéressant de définir des composites, qui permettent de composer plusieurs images existantes pour en obtenir une nouvelle. Cette technique de composition est décrite par le patron Composite.
Il vous est demandé de définir les deux compositions suivantes :
- 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 moins haute,
- la composition « l'une 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.
Une fois ces deux classes définies, ajoutez deux méthodes par défaut à l'interface TextImage
simplifiant la création de leurs instances. Ces méthodes devraient pouvoir s'utiliser ainsi :
// Permet d'obtenir l'image 18x2 : // Un rectangle : ### // ### TextImage.fromString("Un rectangle : ") .leftOf(TextImage.filled(3, 2, '#')); // Permet d'obtenir l'image 4x3 : // XXX // OOOO // OOOO TextImage.filled(3, 1, 'X') .above(TextImage.filled(4, 2, 'O'));
Exercice 4 : dessin d'un échiquier
Utilisez aussi judicieusement que possible les méthodes ajoutées à l'interface TextImage
pour définir l'image d'un échiquier ci-dessous :
+------------------------+ |### ### ### ### | |### ### ### ### | | ### ### ### ###| | ### ### ### ###| |### ### ### ### | |### ### ### ### | | ### ### ### ###| | ### ### ### ###| |### ### ### ### | |### ### ### ### | | ### ### ### ###| | ### ### ### ###| |### ### ### ### | |### ### ### ### | | ### ### ### ###| | ### ### ### ###| +------------------------+
Pour dessiner le cadre autour de cet échiquier, ajoutez encore une méthode de décoration à l'interface TextImage
, 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 au moyen de méthodes existantes, et il n'est donc pas nécessaire de définir une classe pour les images encadrées.