Art ASCII

CS-108 — Série 7

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) {
    drawing().forEach(stream::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 TextImage ci-dessus. Avant d'aller plus loin, importez cette archive dans votre projet.

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 :

  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é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 :

  1. un décorateur permettant de faire une symétrie horizontale d'une image,
  2. 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 :

  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 moins haute,
  2. 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.