Graphisme 2D

Chapitres traités   

Dans cette étude, nous nous intéressons particulièrement au mode graphique, et ainsi nous allons voir comment réaliser des tracés en deux dimensions ainsi que du traitement sur les images. Les classes que nous utiliserons pour dessiner proviennent de six paquetages : java.awt, java.awt.color, java.awt.font, java.awt.geom, java.awt.image et java.awt.print. Ensemble, ces classes constituent l'essentiel de l'API 2D, destinée au tracé de formes, de texte et d'images.


Nous remarquons que ces paquetages sont tous issus de java.awt. Bien qu'actuellement, nous utilisons plutôt la bibliothèque graphique javax.swing, nous remarquons que l'ancienne bibliothèque java.awt peut s'avérer bien utile.

Choix du chapitre Contexte graphique

On appelle contexte graphique une instance de la classe java.awt.Graphics2D. Ce contexte graphique représente une surface de dessin, comme la zone d'affichage d'un composant, une page d'imprimante ou un tampon d'image invisible. Un contexte graphique fournit des méthodes pour réaliser trois sortes d'objets graphiques : formes, textes et images.

L'objet Graphics2D est baptisé contexte graphique car il contient également les informations contextuelles sur la zone de dessin, notamment une zone de masquage, la couleur du tracé, le mode de transfert, la police de caractères et la transformation géométrique de la zone de dessin. Si vous considérez la zone de dessin comme une toile, le contexte graphique sera un chevalet supportant un ensemble d'outils et se distinguant de la zone de dessin.

Depuis la version 1.0 du JDK, la classe Graphics disposait de méthodes pour dessiner des lignes, des rectangles, des ellipses, etc. Mais ces opérations de dessin sont très limitées. Par exemple, vous ne pouvez pas tracer des traits d'épaisseurs différentes ni faire pivoter les formes. Le JDK 1.2 a introduit la bibliothèque Java 2D qui implémente un jeu d'opérations graphiques très puissantes. Ce jeu d'opérations est implémenté par la classe Graphics2D.

Quand avons nous besoin d'un contexte graphique ?

Lorsque nous désirons réaliser des tracés ou travailler sur des images, cela sous-entend, bien entendu, que notre programme soit en mode graphique et que du coup notre fenêtre hérite de la classe JFrame. A partir de là, deux scénarii se présentent. Soit nous traçons directement sur la zone client de la fenêtre, ce qui est tout à fait possible, à condition de créer un contexte graphique. Attention, dans ce mode là, le tracé n'est effectué qu'une seule fois, ce qui sous-entend que le rafraîchissement du tracé n'est pas prévu. Effectivement, en imaginant que nous décidions de placer notre fenêtre momentanément en barre de tâche et qu'ensuite nous demandons de la réafficher, le tracé ne sera pas spécialement demandé puisque rien n'indique de le faire, et du coup le contenu de votre fenêtre sera vierge.

Il existe une méthode qui est spécialisée pour l'affichage des composants graphiques et qui prend donc en compte le rafraîchissement. Cette méthode est automatiquement sollicitée dès que nous avons besoin d'afficher ou de réafficher le composant. Cette méthode s'appelle paintComponent() pour les composants graphiques de la bibliothèque swing et paint() pour les composants issus de la bibliothèque awt.

Lorsque la fenêtre doit se réafficher, soit parce qu'elle se trouvait sur la barre des tâche, soit parce qu'elle se situait en dessous d'une autre fenêtre, le cadre de la fenêtre s'affiche en premier, et ensuite c'est le tour de chacun des composants qui constituent cette fenêtre, comme les boutons, les menus, les labels, les panneaux. C'est à ce moment là que la méthode paintComponent() de chacun de ces composants est sollicité. C'est à l'intérieur de cette dernière que se trouve tout le tracé correspondant au composant, comme par exemple le tracé d'un bouton.

Nous devons donc passer par cette méthode pour être sûr que notre tracé spécifique soit réaffiché au moment du rafraîchissement. Dans ce cas là, nous sommes donc obligés de redéfinir cette méthode. Du coup, cela impose de faire un héritage par rapport à un composant graphique existant. Ainsi, dans cette démarche, nous pouvons par exemple proposer un affichage spécifique sur un bouton. Toutefois, le composant que nous utilisons très fréquemment pour réaliser de nouveaux tracés est le panneau. Il a été justement conçu pour cela. Nous hériterons donc fréquemment de la classe JPanel qui représente ce panneau.

Nous pouvons également prendre la classe JComponent comme conteneur de tracés, qui est certe plus rudimentaire, mais qui du coup prend moins de ressources. Ce composant est en fait l'ancêtre de tous les composants graphiques. C'est justement à partir de cette classe qu'apparaît la méthode paintComponent().

Fenêtre.java
package dessin;

import java.awt.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   public Fenêtre() {
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      this.setSize(300, 250);
      this.setTitle("Test dessins");
      this.getContentPane().setBackground(Color.ORANGE);
      this.getContentPane().add(new Zone());
   }
   public static void main(String[] args) {
      new Fenêtre().setVisible(true);
   }
}

class Zone extends JComponent {
   protected void paintComponent(Graphics g) {
      g.drawArc(-40, 10, 100, 100, 0, 90);
      g.drawLine(10, 10, 10, 60);
      g.drawLine(10, 60, 60, 60);
      g.drawOval(10, 80, 120, 120);
      g.drawPolygon(new int[] {110, 170, 170}, new int[] {60, 60, 10}, 3);
      g.drawPolyline(new int[] {210, 270, 270}, new int[] {60, 60, 10}, 3);
      g.drawRect(10, 80, 120, 120);
      g.drawRoundRect(150, 80, 120, 120, 10, 10);
   }   
}

Pour dessiner des formes dans la bibliothèque Java 2D, vous devez obtenir un objet de la classe Graphics2D. Il s'agit d'une classe fille de la classe Graphics. Effectivement, la méthode paintComponent() reçoit automatiquement en argument un objet de la classe Graphics2D. Toutefois, le paramètre de cette méthode est lui de type Graphics. Vous devez donc effectuer un transtypage dans le cas où vous désirez utiliser la technologie 2D.

public void paintComponent(Graphics g) {
  Graphics2D zone = (Graphics2D)g;
  ...
} 

Lorsqu'une fenêtre demande le rafraîchissement de chacun de ses composants, elle ne fait pas appel directement à la méthode paintComponent() mais passe par l'intermédiaire de la méthode repaint() - dont le nom est d'ailleurs assez explicite. Cette dernière s'occupera elle-même d'appeler, au moment opportun, la méthode paintComponent() . Au préalable, cette méthode repaint() doit s'occuper d'un certain nombre de choses pour que cela se passe dans de bonnes conditions, avec notamment la récupération du contexte graphique.

Il est a noté que nous pouvons avoir besoin de demander de rafraîchir (donc de réafficher) la zone de tracé sans attendre que la fenêtre s'en occupe. Dans ce cas là, il suffit de solliciter cette méthode repaint(). Il est même possible de réafficher qu'une petite partie de votre zone d'affichage. Il suffit alors de définir la zone à raffraîchir en passant un objet Rectangle en paramètre de la méthode repaint(). Seule cette zone rectangulaire sera alors réaffichée.

 

Choix du chapitre Le rendu

L'une des originalités de l'API 2D tient à ce que les formes, le texte et les images sont manipulés pour l'essentiel de la même façon. Le rendu est le procédé qui consiste à prendre un ensemble de formes, textes et images et à définir une façon de les représenter en colorant les pixels sur un écran ou une imprimante. Graphics2D gère quatre opérations de rendu :

  1. Dessiner un contour d'une forme, avec la méthode draw().
  2. Remplir une forme, avec la méthode fill().
  3. Dessiner du texte, avec la méthode drawString().
  4. Dessiner une image, avec l'une des nombreuses formes de la méthode drawImage().

Par rapport aux méthodes proposées par la classe Graphics, l'API Java 2D supporte plusieurs options supplémentaires :

  1. Vous pouvez facilement produire une grande quantité de formes différentes.
  2. L'outil de dessin peut être contrôlé.
  3. Les formes peuvent être remplies aves des couleurs uniformes, des dégradés ou des motifs répétés.
  4. Des transformations permettent de déplacer, d'agrandir, de retrécir, de faire pivoter ou de déformer des formes.
  5. Les formes peuvent être coupées automatiquement pour ne pas déborder d'une zone donnée.
  6. Vous pouvez sélectionner des règles de composition pour décrire la manière dont les pixels d'une nouvelle forme doivent être combinées avec les pixels existants.
  7. Des conseils d'affichage peuvent être fournis pour choisir un compromis entre la vitesse et la qualité de l'affichage.

 

Choix du chapitre Attributs du contexte graphique

Le contexte graphique instancié par un objet Graphics2D est constitué d'attributs ci-dessous, dont les valeurs sont contrôlées par diverses méthodes d'accès :

paint (couleur)
Le paint en cours (un objet de type java.awt.Paint) définit les couleurs de remplissage d'une forme. Cela concerne également les contours et le texte. Il est possible de changer de paint en cours en utilisant la méthode setPaint(). de Graphics2D. La classe Color implémentant l'interface Paint, il est possible de passer des Color à setPaint() lorsque nous désirons utiliser des couleurs unies.
stroke (trait)
Graphics2D utilise le stroke pour déterminer comment dessiner le contour des formes qui sont passées à la méthode draw(). En terminologie graphique, "tracer" une forme signifie prendre un chemin défini par la forme et le dessiner réellement avec un crayon ou un pinceau possédant une taille et des caractéristiques précises. Par exemple, dessiner un cercle en utilisant un tracé se comportant comme une ligne continue produit une forme ressemblant à un anneau ou à une bague. Dans l'API Graphics2D l'objet stroke est un peu plus abstrait que cela. En réalité, il prend la forme à tracer en entrée et renvoie une forme correspondant au contour, qui est ensuite remplie par Graphics2D. Il est possible de fixer le trait courant en utilisant setStroke(). L'API 2D est fournie avec une classe très pratique, java.awt.BasicStrocke, qui permet d'implémenter différentes épaisseurs, extrémités, jointures et pointillés de ligne.
font (police)
Le texte est rendu par une forme représentant des caractères à dessiner. Le font en cours définit les formes créées pour un jeu de caractères donnés. La forme résultante est alors remplie. La police courante est définie à l'aide de setFont(). L'API 2D permet aux applications d'accéder à toutes les polices TrueType et PostScript Type 1 installés sur la machine.
transformation
Les formes, les textes et images sont géométriquement transformés avant d'être rendus. Cela signifie qu'ils peuvent être déplacés, retournés et allongés. La transformation de Graphics2D convertit les coordonnées de l'espace utilisateur en coordonnées de l'espace de périphérique. Par défaut, Graphics2D utilise une transformation faisant correspondre 72 unités de l'espace utilisateur à un pouce sur le périphérique d'affichage. Si vous dessinez une ligne allant du point 0.0 au point 72.0 à l'aide de la transformation par défaut, sa longueur sera d'un pouce, qu'elle soit tracée sur le moniteur ou sur l'imprimante. La transformation courante peut être modifiée à l'aide des méthodes translate(), rotate(), scale() et shear(). La méthode setTransform() permet définir une transformation globale entre l'espace de l'utilisateur et l'espace matériel.
règle de composition
Une règle de composition permet de définir la façon de combiner les couleurs d'une primitive à des couleurs existantes de la surface de dessin d'un Graphics2D. Cet attribut est défini par la méthode setComposite(), qui accepte en argument une intance de java.awt.AlphaComposite. La composition permet de créer des parties de dessin ou d'image totalement ou partiellement transparentes ou de les combiner selon d'autres façons intéressantes.
forme de masquage
Toutes les opérations de rendu sont limitées à l'intérieur de la forme de masquage. Aucun pixel extérieur à cette forme n'est modifié. Par défaut, la forme de masquage permet le rendu de la surface totale du dessin (en général, le rectangle d'un JComponent). Nous pouvons néanmoins le limiter à toute forme simple ou complexe, y compris les formes de texte. La méthode setClip() permet de définir une zone de dessin.
indications de rendu
Diverses techniques permettent le rendu de primitives graphiques. Elle représentent généralement un compromis entre vitesse de rendu et qualité visuelle. Les indications de rendu (constantes définies dans la classe RenderingHints) précisent les techniques à utiliser. La méthode setRenderingHints() permet de définir des conseils d'affichage, qui correspondent à des compromis entre la vitesse et la qualité d'affichage.
Créer une forme
L'API Java 2D fournit plusieurs objets de formes et des méthodes pour combiner ces formes. L'interface Shape représente un objet de forme quelconque.
 

Naturellement, dans un certain nombre de circonstances pratiques, vous n'aurez pas besoin de passer par toutes ces étapes. Il existe en effet des choix par défaut suffisants pour un contexte graphique 2D classique. Vous devrez modifier ces paramètres uniquement si vous souhaitez modifier les valeurs par défaut.

Les diverses méthodes setXXX() définissent simplement l'état du contexte graphique 2D. Elles ne génèrent en aucun cas un dessin directement. De même, lorsque vous construisez des objets Shape, aucun dessin n'est effectué. Une forme est uniquement dessinée lorsque vous appelez draw() ou fill(). A ce moment précis, une nouvelle forme est calculée dans un pipeline d'affichage.

 

Choix du chapitre Pipeline d'affichage

Les primitives graphiques (formes, texte et images) traversent le moteur de rendu suivant une série d'opérations baptisée pipeline d'affichage (ou de rendu). Dans ce pipeline d'affichage, les opérations sont ainsi réalisées suivant une séquence, donc dans un ordre bien précis. Finalement, les étapes suivantes sont nécessaires pour dessiner une forme :

  1. Le contour de la forme est dessinée. Pour les formes dont les contours sont tracés à l'aide de draw(), le trait en cours est utilisé. Le texte est affiché en représentant les caractères par des formes, dans la police en cours.
  2. La forme est transformée.
  3. La forme est restreinte à une zone d'affichage. Masquer le résultat à l'aide de la forme de masquage en cours.
  4. Le reste de la forme est rempli. Déterminer les couleurs à utiliser. Pour une forme remplie, l'objet paint en cours définit les couleurs de remplissage de la forme. Pour une image, les couleurs sont prises dans l'image elle-même.
  5. Les pixels de la forme remplie sont composés avec les pixels existants. Combiner les couleurs avec la surface de dessin existante en utilisant la règle de composition en cours.

 

Choix du chapitre Méthodes proposées de la classe Graphics

Graphics2D comporte quelques méthodes permettant de dessiner et remplir des formes courantes ; elles sont en fait héritées de la classe Graphics.

Méthode Description
drawArc(int x, int y, int largeur, int hauteur, int angledébut, int anglefin)

Dessine un arc de cercle (angle en degré)
drawLine(int xdébut, int ydébut, int xfin, int yfin)

Dessine une ligne
drawOval(int x, int y, int largeur, int hauteur) Dessine un ovale
drawPolygon(int[] lesX, int[] lesY, int nombrePoint) Dessine un polygone et le ferme en joignant les extrémités
drawPolyline(int[] lesX, int[] lesY, int nombrePoint) Dessine une ligne en reliant une série de points, sans la fermer
drawRect(int x, int y, int largeur, int hauteur) Dessine un rectangle
drawRoundRect(int x, int y, int largeur, int hauteur, int largeurArc, int hauteurArc) Dessine un rectangle à coins arrondis
   
fillArc(int x, int y, int largeur, int hauteur, int angledébut, int anglefin) Dessine un arc de cercle plein
fillOval(int x, int y, int largeur, int hauteur) Dessine un ovale plein
fillPolygon(int[] lesX, int[] lesY, int nombrePoint) Dessine un polygone plein
fillRect(int x, int y, int largeur, int hauteur) Dessine un rectangle plein
fillRoundRect(int x, int y, int largeur, int hauteur, int largeurArc, int hauteurArc) Dessine un rectangle plein à coins arrondis

Pour chacune des méthodes fill() du tableau, il existe une méthode draw() correspondante créant la forme selon un dessin non plein. A l'exception de fillArc() et de fillPolygon(), chaque méthode prend une simple indication <x, y> correspondant au coin supérieur gauche de la forme, ainsi qu'une largeur width et une hauteur height.

La méthode la plus souple dessine un polygone, défini par deux tableaux de coordonnées des sommets, d'une part le tableau des x, et d'autre part le tableau des y suivi ensuite du nombre de point. Des méthodes de la classe Graphics lisent ces tableaux et dessinent le contour du polygone ou remplissent celui-ci.

La méthode fillArc() requiert six arguments entiers. Les quatre premiers définissent le rectangle englobant un ovale - comme la méthode fillOval(). Les deux derniers définissent la portion de l'ovale à dessiner, sous forme de position angulaire de départ et de déplacement (tous deux en degrés). La position zéro degré se trouve à trois heures ; l'angle croît dans le sens des aiguilles d'une montre.

Exemples d'utilisation des méthodes de la classe Graphics

Fenêtre.java
package dessin;

import java.awt.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   public Fenêtre() {
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      this.setSize(300, 250);
      this.setTitle("Test dessins");
      this.getContentPane().setBackground(Color.ORANGE);
      this.getContentPane().add(new Zone());
   }
   public static void main(String[] args) {
      new Fenêtre().setVisible(true);
   }
}

class Zone extends JComponent {
   protected void paintComponent(Graphics g) {
      g.drawArc(-40, 10, 100, 100, 0, 90);
      g.drawLine(10, 10, 10, 60);
      g.drawLine(10, 60, 60, 60);
      g.drawOval(10, 80, 120, 120);
      g.drawPolygon(new int[] {110, 170, 170}, new int[] {60, 60, 10}, 3);
      g.drawPolyline(new int[] {210, 270, 270}, new int[] {60, 60, 10}, 3);
      g.drawRect(10, 80, 120, 120);
      g.drawRoundRect(150, 80, 120, 120, 10, 10);
   }   
}

 

Choix du chapitre Approche orientée objet, utilisation des compétences de la classe Graphics2D

Les méthodes que nous venons de voir font parties de la classe Graphics. L'API 2D se sert d'une approche entièrement différente, orientée objet. Au lieu de méthodes, il existe les classes correspondantes suivantes :

  1. Line2D
  2. Rectangle2D
  3. RoundRectangle2D
  4. Ellipse2D
  5. Arcs2D
  6. QuadCurve2D
  7. CubicCurve2D
  8. GeneralPath

Ces classes implémentent toutes l'interface Shape.

Enfin, il existe une classe Point2D qui décrit un point avec des coordonnées x et y. Les points se révèlent utiles pour définir des formes, bien qu'ils n'en soient pas eux-mêmes.

Les classes Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D et Arc2D correspondent respectivement aux méthodes drawLine(), drawRect(), drawRoundRect(), drawOval() et drawArc(). L'API 2D rajoute deux classe supplémentaires : les courbes quadratiques et cubiques. Nous aborderons ces formes un peu plus loin. Il n'existe aucun classe Polygone2D. En remplacement, la classe GeneralPath décrit les tracés composés de lignes, de courbes quadratiques ou cubiques. Vous pouvez utiliser GeneralPath pour décrire un polygone.

Les classes Rectangle2D, RounRectangle2D, Ellipse2D, Arc2D héritent toute d'une superclasse commune, RectangleShape. Indubitablement, les ellipses les arcs ne sont pas des formes rectangulaires, mais ils peuvent être contenus dans un rectangle :

 

Diagramme UML des classes implémentant l'interface Shape

 

Procédure à suivre pour dessiner un objet 2D qui implémente l'interface Shape

Pour dessiner une forme, il faut commencer par créer un objet d'une classe qui implémente l'interface Shape. Il suffit ensuite dans la classe Graphics2D de faire appel uniquement à l'une des deux méthodes spécifiques, soit la méthode draw() pour dessiner le contour de la forme, soit la méthode fill() si vous désirez remplir cette forme.

Voici le codage correspondant du tracé d'un rectangle :


class Zone extends JComponent {
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      Rectangle2D rectangle = new Rectangle2D.Double(10.0, 10.0, 200.0, 100.0);
      surface.draw(rectangle);
   }   
}











Avant l'apparition de la bibliothèque Java 2D, les programmeurs utilisaient les méthodes de la classe Graphics telles que drawRect() pour dessiner des formes. En apparence, les appels de méthode de l'ancien style paraissent plus simple. Cependant, avec la bibliothèque Java 2D, vos options restent ouvertent - vous pouvez ultérieurement améliorer vos dessins à l'aide des nombreux outils que fournit la bibliothèque (transformation, dégradés, transparence, composition entre les formes et les images, etc.).

 

Choix du chapitre Création des formes

L'utilisation des classes de formes Java 2D amène une certaine complexité. Contrairement aux méthodes de la version 1.0, qui utilisaient des entiers pour coordonnées de pixel, Java 2D emploie les valeurs de coordonnées en virgule flottante. Cela est souvent pratique, car vous pouvez spécifier vos formes des coordonnées qui sont significatives pour vous (comme des millimètres par exemple), puis les traduire en pixels.

La bibliothèque Java utilise des valeurs float simple précision pour nombre de ses calculs géométriques est de définir des pixels à l'écran ou sur l'imprimante. Après tout, le but ultime des calculs géométriques est de définir des pixels à l'écran ou sur l'imprimante. Tant que l'erreur d'arrondi reste cantoné à un pixel, l'aspect visuel n'est pas affecté. De plus, les calculs en virgule flottante sont plus rapides sur certaines plate-formes et les valeurs float requièrent un volume de stockage de moitié par rapport aux valeurs double.

Cependant, la manipulation de valeurs float est parfois peu pratique pour le programmeur, car le langage Java est inflexible en ce qui concerne les transtypages nécessaires pour la conversion de valeurs double en valeur float. Effectivement, l'expression suivante est une erreur :

float f = 1.2; // Erreur 1.2 est une contante littérale double

Cette instruction échoue à la compilation, car la constante 1.2 est de type double. Le remède consiste à ajouter le suffixe F à la constante :

float f = 1.2F;

De même, les instructions suivantes produisent le même type d'erreur :

Rectangle2D r = ...
float
f =r.getWidth(); // Erreur

Cette instruction échouera également à la compilation, pour les même raisons. La méthode getWidth() renvoie un double. Cette fois-ci, le remède consiste à prévoir un transtypage :

Rectangle2D r = ...
float
f =(float)r.getWidth(); // Erreur

Les suffixes et les transtypages étant un inconvénient évident, les concepteurs de la bibliothèque 2D ont décidé de fournir deux versions de chaque classe de forme : une avec des coordonnées float pour les programmeurs économes, et une avec des coordonnées double pour les paresseux (je suis de ceux là).

Les concepteurs de la bibliothèque ont à priori choisi une méthode curieuse, et assez déroutante au premier abord. En effet, chacune des classes dont le nom se termine par "2D" est en fait une classe abstraite qui contient deux sous-classes concrètes (classes internes statiques) qui héritent en même temps de cette même classe abstraite, et qui permettent de spécifier les coordonnées, soit sous forme de float, soit sous forme de double.

Phase de création

Ce n'est que lors de lors de la construction d'un objet 2D que vous devrez choisir entre un constructeur avec des coordonnées float ou double :

Rectangle2D floatRectangle = new Rectangle2D.Float(5.0F, 10.0F, 7.5F, 15.0F);
Rectangle2D doubleRectangle = new Rectangle2D.Double(5.0, 10.0, 7.5, 15.0);

Les classes Xxx2D.Float et Xxx2D.Double constituent des sous-classes des classes Xxx2D. Après la construction des objets, il n'existe aucun avantage à se souvenir de la sous-classe et vous pouvez simplement stocker l'objet construit dans une variable de superclasse, comme précédemment.

Comme vous pouvez le voir d'après ces noms curieux, les classes Xxx2D.Float et Xxx2D.Double sont également des classes internes des classes Xxx2D. Plus précisément, il existe deux classes internes Float et Double pour chacune des classes abstraites Xxx2D. Il ne s'agit que d'un côté pratique au niveau de la syntaxe, en vue d'éviter l'augmentation des noms des classes extérieures.

Il est préférable d'ignorer que les deux classes concrètes sont internes statiques, c'est juste une astuce pour éviter d'avoir à fournir des noms tels que FloatRectangle2D et DoubleRectangle2D.

 

Choix du chapitre Formes 2D

Nous allons maintenant passer en revue l'ensemble des classes qui permettent de tracer des formes particulières.

interface Shape

Commençons tout d'abord par l'interface Shape qui dispose un certain nombre de méthodes qui sont impérativement définies dans les classes qui implémentent cette interface.

boolean contains()
Cette méthode, surdéfinie suivant le type de paramètres requis, teste si le point passé en argument se trouve bien à l'intérieur de la zone délimitée par la forme réellement utilisée : rectangle, ellipse, portion d'ellipse, polygone, etc.
Rectangle2D (ou Rectangle) getBounds()
Cette méthode retourne la zone rectangulaire qui englobe la forme réelle.
boolean intersects()
Cette méthode teste si une partie ou la totalité de la zone rectangulaire passée en argument est en contact avec la forme en cours (chevauchement ou pas).
 

Toutes les classes concrètes qui implémentent Shape doivent impérativement redéfinir ces méthodes, puisque dans le cas d'une interface, elles sont justes déclarées. Nous imaginons bien, par exemple, que pour indiquer si un point fait parti de la zone interne d'une forme, ce n'est pas du tout la même chose entre un rectangle et une ellipse. Il faut donc que chaque classe propose le test adéquat.

 

Classe abstraite RectangularShape

Les classes comme Rectangle2D et Ellipse2D héritent toutes deux d'une superclasse commune RectangularShape. Bien sûr, comme nous l'avons déjà vus, les ellipses ne sont pas rectangulaires, mais elles sont incluses dans un rectangle englobant.

La classe RectangularShape définit plus de 20 méthodes qui sont communes à ces formes, parmi lesquelles les incontournables getWidth(), getHeight(), getCenterX(), getCenterY().

 

Les rectangles et les ellipses

Les objets Rectangle2D et Ellipse2D sont simples à construire. Vous devez spécifier :

  1. Les coordonnées x et y du coin supérieur gauche ;
  2. La largeur et la hauteur.

En voici le canevas :

Rectangle2D rectangle = new Rectangle2D.Double(x, y, largeur, hauteur);
Ellipse2D
ellipse = new Ellipse2D.Double(x, y, largeur, hauteur);

Pour les ellipses, ces valeurs font référence au rectangle englobant. Par exemple :

Ellipse2D ellipse = new Ellipse2D.Double(150, 200, 100, 50);

construit une ellipse englobée dans un rectangle dont le coin supérieur gauche a les coordonnées (150, 200), avec une largeur de 100, et une hauteur de 50.


Il arrive que vous ne disposiez pas directement des valeurs du coin supérieur gauche. Il est assez fréquent d'avoir les deux coins opposés de la diagonale d'un rectangle. Vous devez d'abord créer un rectangle vide et utilisez ensuite la méthode setFrameFromDiagonal(), soit en passant les coordonnées du point haut gauche et du point bas droit, soit directement avec les points représentatifs en tant qu'objet Point2D :

Rectangle2D rectangle = new Rectangle2D.Double();
rectangle
.setFrameFromDiagonal(xdébut, ydébut, xfin, yfin);

ou alors :

Rectangle2D rectangle = new Rectangle2D.Double();
...
Point2D début = new Point2D.Double(xdébut, ydébut);
Point2D
fin = new Point2D.Double(xfin, yfin);
...
rectangle
.setFrameFromDiagonal(début, fin);


Lors de la construction d'une ellipse, vous connaissez généralement le centre, la largeur et la hauteur, mais pas les popints des angles du rectangle englobant (qui ne repose pas sur les ellipses). Il existe une méthode setFrameFromeCenter() qui utilise le point central, malheureusement elle requiert toujours l'un des quatre points d'angle. Pour tracer la même ellipse vue plus haut, et en utilisant le centre comme critère, voici ce que nous devons coder :

Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(200, 225, 250, 250);

Les méthodes setFrameFromDiagonal() et setFrameFromeCenter() sont en fait des méthodes de RectangularShape. Vous pouvez donc les utiliser pour n'importe quelle classe fille, donc aussi bien pour Rectangle2D que pour Ellipse2D.

Voici un exemple traitant des méthodes que nous venons d'évoquer :


class Zone extends JComponent {
   private Ellipse2D ellipse = new Ellipse2D.Double();
   private Rectangle2D rectangle = new Rectangle2D.Double();
   
   public Zone() {
      Rectangle2D r = new Rectangle2D.Double(50, 50, 190, 110);
      ellipse.setFrameFromCenter(r.getCenterX(), r.getCenterY(),  r.getMaxX(), r.getMaxY());
      rectangle.setFrameFromDiagonal(ellipse.getX(), ellipse.getY(), r.getMaxX(), r.getMaxY());
   }
   
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      surface.draw(rectangle);
      surface.draw(ellipse);
   }   
}


Cet exemple sert à montrer l'utilisation des méthodes qui peuvent être intéressantes dans certaines applications. Ici, nous aurions pu, bien entendu, créer l'ellipse et le rectangle directement àau moment de l'appel des constructeurs en spécifiant les coordonnées x et y avec la largeur et la hauteur.

 

Rectangle arrondi

La classe RoundRectangle2D permet de tracer des rectangles avec les coins arrondis. Pour cette forme, vous devez aussi spécifier les coordonnées du coin supérieur gauche, la largeur, la hauteur ainsi que les dimensions en x et en y de la zone qui doit être arrondie.

RoundRectangle2D rectangle = new RoundRectangle2D.Double(x, y, largeur, hauteur, largeurArc, hauteurArc);

 

Arc de cercle

La classe qui implémente l'arc de cercle est la classe Arc2D. Pour construire un arc, il faut fournir un rectangle entourant cet arc, suivi de l'angle de départ et de l'angle d'ouverture, ainsi que du type de fermeture, qui peut être Arc2D.OPEN, Arc2D.PIE ou Arc2D.CHORD.

Attention, les angles s'expriment en dégré.
.

Arc2D arc = new Arc2D.Double(x, y, largeur, hauteur, angleDépart, angleArc, typeFermetureArc);

Voici un exemple de demi-cercle dont l'arc de départ est fixé à 60°.

class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      Arc2D arc = new Arc2D.Double(10, 10, 200, 200, 60, 180, Arc2D.PIE);
      surface.draw(arc);
   }   
}











Ici, nous pouvons choisir comme type de fermeture aussi bien Arc2D.PIE que Arc2D.CHORD.

 

Choix du chapitre Courbes 2D

Dans ce chapitre, nous allons voir comment tracer des lignes, des courbes quadratiques et cubiques ainsi qu'un assemblage de ces différents types de tracé.

Tracé de segments de lignes

le tracé des segments de lignes s'obtient au moyen de la classe Ligne2D. Pour construire une ligne, vous fournissez les points de départ et d'arrivé, sous la forme d'objets Point2D ou de paires de nombres.

Point2D début = new Point2D.Double(xdébut, ydébut);
Point2D
fin = new Point2D.Double(xfin, yfin);
...
Ligne2D
ligne = new Ligne2D.Double(début, fin);

ou directement :

Ligne2D ligne = new Ligne2D.Double(xdébut, ydébut, xfin, yfin);

 

Tracé de courbes

La bibliothèque Java 2D fournit des courbes quadratiques représentées par la classe QuadCurve2D et des courbes cubiques représentées par la classe CubicCurve2D. Les courbes quadratiques et cubiques sont spécifiées par deux points de terminaison, et un ou deux points de contrôle. C'est d'ailleurs le nombre de point de contrôle qui détermine le type de courbe. Ainsi, la forme des courbes peut être modifié en changeant les points de contrôle.

QuadCurve2D q = new QuadCurve2D.Double(xdébut, ydébut, contrôleX, contrôleY, xfin, yfin);
CubicCurve2D c = new CubicCurve2D.Double(xdébut, ydébut, contrôleDébutX, contrôleDébutY, contrôleFinX, contrôleFinY, xfin, yfin);

Les courbes quadratiques ne sont pas très flexibles, et elles ne sont pas utilisées très souvent. Les courbes cubiques (courbes de Bezier) sont bien plus répandues. En combinant plus courbes cubiques, de sorte que les tangentes soient respectées aux points de connexion, vous pouvez créer des formes arrondies complexes.

 

 

class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);
      surface.draw(pétale);
   }   
}

 

Combinaison de plusieurs tracés

Il est possible de construire des séquences arbitraires de segments de lignes, de courbes quadratiques et de courbes cubiques, et de les enregistrer dans un objet GeneralPath. Voici la procédure à suivre :

  1. Création d'un objet GeneralPath vierge,
  2. La première coordonnée du tracé peut alors être spécifiée à l'aide de la méthode moveTo(),
  3. Ensuite, il faut étendre le tracé en appelant l'une des méthodes suivantes : lineTo(), quadTo(), ou curveTo(). Ces méthodes étendent respectivement le tracé avec une ligne, une courbe quadratique ou une courbe cubique. pour appeler lineTo(), fournissez le point de fin. Pour les deux méthodes de courbes, fournissez les points de contrôle, puis le point de fin (dans tous les cas, vous n'avez pas besoin de fournir le point de début, puisqu'il correspond au point de fin du tracé précédent).
  4. Le tracé peut être fermé en appelant la méthode closePath(). Elle affiche une ligne entre le point courant et le dernier moveTo().

Pour créer un polygone, appelez simplement moveTo() pour aller au premier angle, suivi d'appels répétés lineTo() pour parcourir les autres points. Pour terminer, appeler closePath() pour fermer le polygone.

class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      GeneralPath triangle = new GeneralPath();
      triangle.moveTo(10, 10);
      triangle.lineTo(10, 100);
      triangle.lineTo(100, 100);
      triangle.closePath();
      surface.draw(triangle);
   }   
}




Un tracé général n'a pas besoin d'être connecté. Vous pouvez appeler moveTo() à n'importe quel moment pour commencer un nouveau segment de tracé.
.

Pour terminer, vous pouvez avoir recours à la méthode append() pour ajouter des objets Shape à un tracé général. Le contour de la forme est alors ajouté à la fin du tracé. Le second paramètre de la méthode append() est true si la nouvelle forme doit être connectée au premier point du tracé, ou false dans le cas contraire.

class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      GeneralPath dessin = new GeneralPath();
      dessin.moveTo(150, 140);
      dessin.curveTo(10, 0, 290, 0, 150, 140);
      dessin.append(new Ellipse2D.Double(120, 140, 60, 60), false);
      surface.draw(dessin);
   }   
}




 

Choix du chapitre Zone

Dans la section précédente, vous avez vu comment générer des formes complexes en construisant des tracés généraux composés de lignes et de courbes. En utilisant un nombre suffisant de lignes et de courbes, vous pouvez représenter quasiment n'importe quelle forme. Par exemple, les formes des caractères des polices visibles sur un écran ou sur un papier imprimé sont toutes composées de lignes et de courbes cubiques.

Cependant, et de manière occasionnelle, il est plus simple de décrire une courbe en la décomposant en zones, comme des rectangles, des polygones ou des ellipses. L'API Java 2D dispose d'un objet de zone représenté par la classe Area. Cette classe supporte quatre opérations de combinaisons de zones géométriques, qui mélangent les pixels de deux zones pour former une nouvelle zone :

  1. add() : la zone résultat contient tous les points qui se trouvent dans la première zone ou dans la seconde.
  2. substract() : la zone résultat contient tous les points de la première zone qui ne sont pas dans la seconde zone.
  3. intersect() : la zone résultat contient tous les points qui se trouvent dans la première zone et dans la seconde.
  4. exclusiveOr() : la zone résultat contient tous les points qui se trouvent dans la première zone ou dans la seconde, mais pas dans les deux.

Pour construire une zone complexe, il faut commencer avec un objet de zone par défaut (vierge). Puis, vous pouvez combiner cette zone avec n'importe quelle autre forme. La classe Area implémente l'interface Shape. Vous pouvez alors dessiner contours de zone avec la méthode draw(), ou en remplir l'intérieur avec la méthode fill() de la classe Graphics2D.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      
      GeneralPath dessin = new GeneralPath();
      dessin.moveTo(150, 140);
      dessin.curveTo(10, 0, 290, 0, 150, 140);
      dessin.append(new Ellipse2D.Double(120, 140, 60, 60), false);
      
      Area zone = new Area();
      zone.add(new Area(new Rectangle2D.Double(70, 20, 160, 190)));
      zone.subtract(new Area(dessin));
      surface.fill(zone);
   }   
}



 

Choix du chapitre Outils de dessin

L'opération draw() de la classe Graphics2D dessine le contour d'une forme en utilisant l'outil de dessin courant. Par défaut, cet outil est une ligne pleine d'un seul pixel d'épaisseur. Vous pouvez sélectionner un autre outil en appelant la méthode setStroke(). Il suffit de fournir un objet d'une classe qui implémente l'interface Stroke. En fait, l'API Java 2D définit une seule classe adaptée, il s'agit de BasicStroke.

Epaisseur du trait

Ainsi, nous pouvons construire des outils de dessin de n'importe quel épaisseur :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;

      GeneralPath dessin = new GeneralPath();
      dessin.moveTo(180, 30);
      dessin.lineTo(105, 105);
      dessin.moveTo(30, 30);
      dessin.lineTo(30, 180);
      dessin.lineTo(180, 180);
      dessin.closePath();
      
      surface.setStroke(new BasicStroke(20));
      surface.draw(dessin);
   }   
}


 

Etrémités du trait

Lorsqu'un outil fait plus d'un pixel de large, son extrémité peut prendre différents aspects :

  1. Une extrémité droite : l'outil s'arrête exactement au dernier point tracé - BasicStroke.CAP_BUTT.
  2. Une extrémité arrondie : un demi-cercle est ajouté à la fin du tracé - BasicStroke.CAP_ROUND.
  3. Une extrémité carré : un demi carré est ajouté à la fin du tracé - BasicStroke.CAP_SQUARE.

 

Intersection de deux traits

Lorsque deux tracés épais se croisent, il existe trois possibilités pour le style de l'intersection :

  1. Une intersection en biais : les deux tracés sont reliés par une ligne droite perpendiculaire à la bissectrice de l'angle entre les deux tracés - BasicStroke.JOIN_BEVEL.
  2. Une intersection arrondie : les deux tracés sont prolongés par une extrémitée arrondie - BasicStroke.JOIN_ROUND.
  3. Une intersection angulaire : les deux tracés sont prolongés par un angle - BasicStroke.JOIN_MITER.

L'intersection angulaire n'est pas adaptée aux lignes qui se croisent selon un angle aigu. Si deux lignes se croisent selon un angle inférieur à la limite angulaire, une intersection enbiais est choisie automatiquement en remplacement. Cela permet d'éviter les prolongements angulaires trop longs. Par défaut, cette limite angulaire est fixée à dix degrés.

 

Mise en oeuvre des extrémités et des intersections des traits

Pour spécifier les extrémités et les intersections, vous devez utiliser encore une fois la méthode setStroke(). Vous passez également en argument de cette méthode un objet de type BasicStroke, mais cette fois-ci en construisant l'objet à l'aide de trois ou quatre arguments :

surface.setStroke(new BasicStroke(épaisseur, extrémité, intersection, limite angulaire (en option)));

En reprenant le graphisme de tout à l'heure, et si nous désirons avoir un tracé plutôt arrondi, voilà comment procéder :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;

      GeneralPath dessin = new GeneralPath();
      dessin.moveTo(180, 30);
      dessin.lineTo(105, 105);
      dessin.moveTo(30, 30);
      dessin.lineTo(30, 180);
      dessin.lineTo(180, 180);
      dessin.closePath();
      
      surface.setStroke(new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
      surface.draw(dessin);
   }   
}


 

Motif de remplissage

Pour terminer, vous pouvez choisir des lignes pointillées en définissant un motif de remplissage. Un motif de remplissage est composé d'un tableau float[] de nombres contenant la longueur des tirets blancs et noirs.

Le motif de remplissage et la phase de remplissage peuvent être spécifiés lors de la construction du BasicStroke. Toutefois, cette partie s'intègre à la suite de la définition des extrémités et des intersections de traits. La phase de remplissage indique la position de démarrage de chaque ligne par rapport au motif de remplissage. Normalement cette valeur vaut 0.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;

      GeneralPath dessin = new GeneralPath();
      dessin.moveTo(180, 30);
      dessin.lineTo(105, 105);
      dessin.moveTo(30, 30);
      dessin.lineTo(30, 180);
      dessin.lineTo(180, 180);
      dessin.closePath();
      
      float[] motif = {5, 10, 5, 10, 20, 10};
      surface.setStroke(
         new BasicStroke(5,                  // épaisseur
               BasicStroke.CAP_ROUND,   // extrémité
               BasicStroke.JOIN_MITER,   // intersection
               15,                                       // limite angulaire
               motif,                   // motif de remplissage                
               0                          // phase de remplissage
         ));
      surface.draw(dessin);
   }   
}

 

Choix du chapitre Remplissage

Lorsqu'une forme est remplie, son intérieur est recouvert d'une couleur de remplissage. La méthode setPaint() permet de choisir un style de remplissage parmi plusieurs objets qui implémentent l'interface Paint. Dans l'API Java 2D, il existe trois classes adéquates suivant le type de remplissage voulu :

  1. Couleurs unies : la classe Color implémente l'interface Paint pour remplir des formes avec une couleur unie.
  2. Dégradés : la classe GradientPaint fait varier la couleur de remplissage en l'interpolant entre deux valeurs spécifiées.
  3. Textures : La classe TexturePaint remplit une zone en répétant une image.

Ces types de remplissages peuvent tout aussi bien être utilisés pour l'apparence du trait (du contour) que pour l'apparence du fond de la forme choisie. Suivant le cas, il suffit alors de faire appel, soit à la méthode draw(), soit à la méthode fill() de la classe Graphics2D.

 

Couleurs unies

La méthode setPaint() de la classe Graphics2D permet de sélectionner une couleur qui sera employée par toutes les opérations de dessin ultérieures pour le contexte graphique. Pour dessiner avec plusieurs couleurs, effectuer une opération, puis sélectionner une autre couleur avant de procéder à l'opération suivante.

Les couleurs sont définies à l'aide de la classe Color. La classe java.awt.Color propose des constantes prédéfinies pour les treize couleurs standard indiqué dans le tableau ci-dessous.

Constantes prédéfinies Couleur standard correspondante

Color.black
Color.blue
Color.cyan
Color.darkGray
Color.gray
Color.green
Color.lightGray
Color.magenta
Color.orange
Color.pink
Color.red
Color.white
Color.yellow

noir
bleu
cyan (bleu clair)
gris foncé
gris
vert
gris clair
magenta
orange
rose
rouge
blanc
jaune

surface.setPaint(Color.red);

Vous pouvez spécifier une couleur personnalisée en créant un objet Color à l'aide de ses composantes rouge, verte et bleue. En utilisant une échelle de 0 à 255 (chaque composante est codée sur un octet) pour les proportions de rouge, de vert et de bleu, appelez le constructeur de Color de la façon suivante :

surface.setPaint(new Color(0, 128, 128)); // un bleu-vert foncé

Voici un exemple de définition de couleur à la fois sur le trait ainsi que sur le fond des formes constituant le tracé de l'application :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      
      CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);
      surface.setPaint(Color.YELLOW);
      surface.fill(pétale);
      
      Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60);
      surface.setStroke(new BasicStroke(10));
      surface.draw(cercle);
      
      surface.setPaint(new Color(255, 128, 0));
      surface.fill(cercle);
   }   
}


Les méthodes brighter() et darker() de la classe Color produisent des versions plus vives ou plus foncées de la couleur actuelle. La méthode brighter() permet de mettre un élément en surbrillance, mais avive en réalité à peine la couleur. Pour obtenir une couleur plus visible, appelez plusieurs fois la méthode.

couleur.brighter().brighter().brighter();

 

Dégradés

Un dégradé de couleurs est un passage graduel d'une couleur à une autre. La classe GradientPaint encapsule ce concept dans une implémentation d'interface Paint. Il nous suffit d'indiquer deux points de la couleur de chaque point. GradientPaint s'occupe de tous les détails pour faire fondre progressivement la couleur d'un point à l'autre.

Les objets GradientPaint peuvent être construits en fournissant deux points et les couleurs de ces deux points :

surface.setPaint(new GradientPaint(premierPoint, couleurPremierPoint, deuxièmePoint, couleurDeuxièmePoint));

Les couleurs sont interpolées le long de la ligne joignant ces deux points et elles sont constantes le long des lignes perpendiculaires à cette ligne. Les points situés après le dernier point de cette ligne prennent la couleur du dernier point de cette ligne.

Sinon, vous pouvez appeler le constructeur GradientPaint (avec un paramètre supplémentaire) en choisissant true pour le paramètre cyclique :

surface.setPaint(new GradientPaint(premierPoint, couleurPremierPoint, deuxièmePoint, couleurDeuxièmePoint, cyclique));

Le dernier paramètre de GradientPaint définit donc si le gradient est cyclique. Dans un gradient cyclique, les couleurs continuent de fluctuer au-delà des deux points indiqués. Sinon (si la valeur est false), le gradient dessine un mélange unique d'un point à l'autre. Au-delà de chaque point extrême, la couleur est unie.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      
      CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);
      surface.setPaint(new GradientPaint(
         10, 10, // ou alors un seul paramètre new Point2D.Double(10, 10),
         Color.red,
         30, 30, // ou alors un seul paramètre new Point2D.Double(30, 30),
         Color.yellow,
         true
      ));
      surface.fill(pétale);
      
      Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60);
      surface.setStroke(new BasicStroke(10));
      surface.draw(cercle);
      
      surface.setPaint(new GradientPaint(
         120, 140, // ou alors un seul paramètre new Point2D.Double(120, 140),
         Color.blue,
         180, 200, // ou alors un seul paramètre new Point2D.Double(180, 200),
         Color.cyan
      ));
      surface.fill(cercle);
   }   
}

 

Textures

Une texture est une image continuellement répétée, comme un carrelage. Ce concept est représenté dans l'API 2D par la classe TexturePaint. Pour créer une texture, il suffit d'indiquer l'image à utiliser et le rectangle qui servira à la reproduire.

Vous avez vu comment créer des images simples en traçant des lignes et des formes. Des images complexes, telles que des photographies, sont habituellement générées en externe, par exemple avec un scanner ou un logiciel dédié à la manipulation d'images. Il est également possible de produire une image pixel par pixel, et de stocker le résultat dans un tableau.

Pour construire un objet TexturePaint, il faut spécifier une BufferedImage et un rectangle d'origine. Ce rectangle est prolongé indéfiniment sur les deux axes (x et y) pour remplir entièrement le plan. L'image est agrandie ou retrécie pour remplir exactement le rectangle d'origine, puis reproduite dans tous les autres rectangles.

Vous pouvez créer un objet BufferedImage en fournissant la taille de l'image et son type. Le type le plus répandu est BufferedImage.TYPE_INT_ARGB, dans lequel chaque pixel est spécifié par un entier décrivant les valeurs de rouge, de vert, de bleu et de transparence (ou alpha)

BufferedImage image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB);

Ensuite, vous obtenez un contexte graphique vous permettant de dessiner dans votre image :

Graphics2D zoneImage = image.createGraphics();

Toutes les opérations de dessin dans zoneImage ont maintenant pour effet de remplir l'image intermédiaire avec des pixels. Lorsque vous avez fini, vous pouvez créer votre objet TexturePaint :

surface.setPaint(new TexturePaint(image, rectangle));

Voici un exemple où nous fabriquons la texture de toute pièce :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      
      CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);

      BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
      Graphics2D zoneImage = image.createGraphics();
      zoneImage.setPaint(Color.red);
      zoneImage.draw(new Ellipse2D.Double(0, 0, 10, 20));
      Rectangle2D rectangle = new Rectangle2D.Double(0, 0, image.getWidth(), image.getHeight());

      surface.setPaint(new TexturePaint(image, rectangle));
      surface.fill(pétale);
      
      Ellipse2D ellipse = new Ellipse2D.Double(120, 140, 60, 60);
      surface.setStroke(new BasicStroke(30));
      surface.draw(ellipse);
      
      surface.setPaint(new GradientPaint(
         120, 140, // ou alors un seul paramètre new Point2D.Double(120, 140),
         Color.blue,
         180, 200, // ou alors un seul paramètre new Point2D.Double(180, 200),
         Color.cyan
      ));
      surface.fill(ellipse);
   }   
}

Dans cet exemple, nous avons choisi de prendre un rectangle dont la dimension correspond à la taille de l'image. Rien n'empêche de choisir des dimensions du rectangle qui soit un multiple de la taille de l'image, par exemple, deux fois la largeur sur trois fois la hauteur.

Il est possible également de récupérer une texture depuis un fichier image sur le disque dur. La classe ImageIO est spécialement conçue pour cela et simplifie la lecture d'un fichier graphique dans une image intermédiaire.

BufferedImage image = ImageIO.read(new File("fichier image.gif"));

 

Choix du chapitre Tracé de texte

Tout comme le tracé du contour d'une forme, celui du texte n'est qu'une variante du remplissage d'une forme. Lorsque nous demandons à un Graphics2D de dessiner un texte, il détermine les formes nécessaires au dessin et les remplit. Les formes représentant des caractères sont baptisées Glyphes. Une police est une collection de glyphes.

Pour dessiner un texte, nous faisons appel à la méthode drawString() de Graphics2D qui existait déjà dans la classe mère Graphics, mais qui a été redéfinie afin de permettre les différents traitements (remplissage, tranformations, clipping, etc.) déjà utilisés par les formes (Shape) plus classiques.

surface.drawString("Bienvenue ! ", 50, 150));

Lorsque nous appelons drawString(), Graphics2D utilise la police courante pour récupérer les glyphes correspondant aux caractères de la chaîne. Puis les glyphes (qui ne sont pas des Shape) sont remplis à l'aide du Paint en cours.

Graphics2D.drawString(texte à afficher , x, y));

 

Changement de police

Il est possible d'écrire un texte avec une police différente que celle prévue par défaut. Les polices de caractères, appelées aussi fontes, sont représentées par des objets de la classe java.awt.Font. Un objet Font est construit à partir d'un nom de police, un identificateur de style et d'une taille.

Font police = new Font(nom de police, style, taille);

Il existe trois sortes de noms de polices : les familles, les caractères (également baptisés noms de polices) et les noms logiques. Les noms de famille et de police sont étroitement liés. Par exemple, "Garamond Italic" est une police de la famille "Garamond".

 

Police logique

Un nom logique est un nom générique attribué à la famille. Les noms des polices logiques ci-dessous sont en général disponibles sur toutes les plate-formes :

Le nom logique est apparié à une police présente sur la plate-forme locale. Les fichiers fonts.properties de Java font correspondre les noms de polices aux polices disponibles, en recouvrant le plus de caractères Unicode possible. Si nous demandons une police inexistante, nous obtenons la police par défaut. L'une des plus belles avancées de l'API 2D est quelle sait utiliser la plupart des polices installées sur l'ordinateur.

 

Paramètres de la classe Font

Pour écrire des caractères d'une fonte, vous devez donc d'abord créer un objet de la classe Font. Vous spécifier alors la chaîne de caractère correspondant au nom de la fonte, le style de la fonte ainsi que sa taille.

Font verdana = new Font("Verdana", Font.BOLD, 14);

Le troisième paramètre est la taille en points. Le point est souvent utilisé en typographie pour indiquer la taille d'une police. Un pouce comprend 72 points.
.

Le nom de fonte logique peut être employé dans le constructeur de Font. Il faut ensuite spécifier le style (normal, gras, italique ou gras italique) dans le second paramètre du constructeur, en lui donnant une des valeurs suivantes :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;    
      surface.setPaint(new GradientPaint(
         10, 10, 
         Color.red,
         30, 30, 
         Color.yellow,
         true
      ));
      surface.setFont(new Font("SansSerif", Font.ITALIC+Font.BOLD, 48));
      surface.drawString("Bienvenue !", 10, 50);
   }   
}

 

Changement d'un seul paramètre de la police par défaut

Parfois, plutôt que de créer une nouvelle police, il peut être judicieux de changer un seul des paramêtres de la police déjà utlisée. C'est le cas notamment pour changer uniquement le style ou la taille de la police par défaut (ou une autre déjà existante). Pour cela utilisez la méthode deriveFont() de la classe Font. Cette méthode est surchargée pour permettre le changement, soit du style de la fonte (le paramètre est de type int), soit la taille de la fonte (le paramètre est de type float) :

fonte.deriveFont(Font.BOLD); // change la style de la police
fonte.deriveFont(48F); // change la taille de la police

Par contre, si vous décidez de changer le nom de la police, vous êtes, cette fois-ci, obligés de spécifier tous les paramètres et donc de créer une nouvelle fonte, comme nous l'avons montré dans la rubrique précédente.

Pour récupérer la fonte de la police utilisée par défaut, il suffit de faire appel à la méthode getFont() qui fait systématiquement partie de tous les composants graphiques tel que JComponent.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;    
      surface.setPaint(new GradientPaint(
         10, 10, 
         Color.red,
         30, 30, 
         Color.yellow,
         true
      ));
      surface.setFont(this.getFont().deriveFont(48F));
//      surface.setFont(new Font("SansSerif", Font.ITALIC+Font.BOLD, 48));
      surface.drawString("Bienvenue !", 10, 50);
   }   
}

 

Taille d'une police de caractères

Nous allons maintenant centrer la chaîne dans la fenêtre au lien de l'écrire à une position arbitraire. Nous devons alors connaître la largeur et la hauteur de la chaîne en pixels. Ces dimensions dépendent de trois facteurs :

  1. La police utilisée.
  2. La chaîne (ici "Bienvenue").
  3. L'unité sur laquelle la chaîne est écrite (ici, l'écran de l'ordinateur).

Pour obtenir un objet qui représente les caractéristiques de fonte à l'écran, appelez la méthode getFontRenderContext() de la classe Graphics2D. Elle renvoie un objet de la classe FontRenderContext. Dès lors, il est possible de récupérer le rectangle qui englobe la chaîne au moyen de la méthode getStringBounds() de la classe Font :

FontRenderContext contexte = surface.getFontRenderContext();
Rectangle2D rectangle = fonte.getStringBounds("Bienvenue !", contexte);

Pour interpréter les dimensions de ce rectangle, il est utile de connaître certains termes de typographie :

  1. La ligne de base (baseline) : est la ligne imaginaire sur laquelle s'aligne la base des caractères comme "e".
  2. Le jambage ascendant (ascent) : représente la distance entre la ligne de base et la partie supérieure d'une lettre "longue du haut", comme "b" ou "k" ou un caractère majuscule.
  3. Le jambage descendant (descent) : représente la distance entre la ligne de base et la partie inférieure d'une lettre "longue du bas", comme "p" ou "g".
  4. L'interligne (leading) : est l'intervalle entre la partie inférieure d'une ligne et la partie supérieure de la ligne suivante.
  5. La hauteur (height) : est la distance verticale entre deux lignes de base successives et équivaut à (jambage descendant + interligne + jambage ascendant).

La largeur (width) du rectangle que renvoie la méthode getStringBounds() est l'étendue horizontale de la chaîne. La hauteur du rectangle est la somme des jambages ascendant, descendant et de l'interligne. L'origine du rectangle se trouve à la ligne de base de la chaîne. La coordonnée y supérieure du rectangle est négative.

Vous pouvez obtenir les valeurs de largeur, de hauteur et des jambages ascendant d'une chaîne de la façon suivante :

FontRenderContext contexte = surface.getFontRenderContext();
Rectangle2D rectangle = fonte.getStringBounds("Bienvenue !", contexte);
double largeur = rectangle.getWidth();
double hauteur = rectangle.getHeight();
double ascendant = -rectangle.getY();

Voici l'exemple qui permet donc de centrer le texte "Bienvenue!" dans le sens de la hauteur et dans le sens de la largeur :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;    
      surface.setPaint(new GradientPaint(10, 10, Color.red, 30, 30, Color.yellow, true));
      String message = "Bienvenue !";
      Font police = this.getFont().deriveFont(48F);
      surface.setFont(police);
      FontRenderContext contexte = surface.getFontRenderContext();
      Rectangle2D rectangle = police.getStringBounds(message, contexte);
      
      // coin supérieur gauche du texte
      double x = (this.getWidth() - rectangle.getWidth()) / 2;
      double y = (this.getHeight() - rectangle.getHeight()) / 2;
      
      // ajouter jambage ascendant à y pour atteindre la ligne de base
      double ascendant = -rectangle.getY();
      surface.drawString(message, (int) x, (int)(y+ascendant));
   }   
}

Si vous souhaitez connaître le jambage descendant ou l'interligne, vous devez appeler la méthode getLineMetrics() de la classe Font. Elle renvoie un objet de la classe LineMetrics, possédant les méthodes permettant d'obtenir les dimensions intrinsèques d'une police de caractères, comme :

  1. getAscent() : jambage ascendant.
  2. getLeading() : interligne.
  3. getDescent() : jambage descendant.
  4. getHeight() : hauteur.

FontRenderContext contexte = surface.getFontRenderContext();
LineMetrics dimensions = fonte.getLineMetrics("Bienvenue !", contexte);
float ascendant = dimensions.getAscent();
float hauteur = dimensions.getHeight();
float descendant = dimensions.getDescent();
float interligne = dimensions.getLeading();

 

Diagramme UML récapitulant les différentes classes utilisées

 

Choix du chapitre Dessin d'images

Les images sont traitées un peu différemment des formes. En particulier, nous ne faisons pas appel au Paint courant pour dessiner une image car celle-ci contient ses propres informations de couleurs.

Comme pour le texte, la classe Graphics2D dispose de la méthode spécifique drawImage() qui permet de placer l'image à l'emplacement indiqué. Cette méthode attend toutefois en premier argument l'objet de type Image qui représente l'image réelle récupérée à partir du disque dur. Nous avons déjà découvert la classe ImageIO qui permet de récupérer un tel fichier.

Image image = ImageIO.read(new File("fichier image.gif"));
surface.drawImage(image, 50, 150, null));

Par ailleurs, la méthode read() de la classe ImageIO peut récupérer une image à partir d'une URL :

Image image = ImageIO.read(new URL("http://site/répertoire/fichier image.gif"));
surface.drawImage(image, 50, 150, null));

La variable image contient alors une référence à un objet qui encapsule les données images. Vous pouvez afficher l'image grâce à la méthode drawImage() de la classe Graphics.

surface.drawImage(image, positionX, positionY, spectateur));

La classe java.awt.Image représente une vue d'une image. La vue est créée à partir d'une image source fournissant des données sous la forme de pixels. Les images peuvent provenir d'une source statique, comme des fichiers GIF, JPAG, PNG ou dynamique comme un stream d'animation ou un moteur graphique. La classe Image de Java 2 gère également l'animation GIF89a (gifs animés), de sorte qu'il est aussi facile de travailler avec des animations simples qu'avec des images statiques.

Voici un exemple où nous affichons une image à partir du coin supérieur gauche :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 0, 0, null);
      } 
      catch (IOException e) {
         surface.drawString("Image inexistante", 10, 10);
      }      
   }   
}






Nous allons reprendre le même exemple, mais cette fois-ci nous tenons compte de la dimension de l'image afin qu'elle soit toujours centrée :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         Image image = ImageIO.read(new File("chouette.jpg"));
         double positionX = (this.getWidth()-image.getWidth(null)) / 2;
         double positionY = (this.getHeight()-image.getHeight(null)) / 2;
         surface.drawImage(image, (int)positionX, (int)positionY, null);
      } 
      catch (IOException e) {
         surface.drawString("Image inexistante", 10, 10);
      }     
   }   
}




 

Imposer une taille à l'image

Une autre version de la méthode drawImage() permet de changer l'échelle d'une image. Cette version utilise deux arguments supplémentaires pour désigner la largeur et la hauteur voulues quelle que soit les dimensions originales de l'image. Cette méthode permet donc de réaliser un reéchantillonage :

surface.drawImage(image, positionX, positionY, largeur, hauteur, spectateur));

Voici un exemple qui adapte la taille de l'image à la dimension de la fenêtre (Attention, le ratio n'est pas respecté) :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 0, 0, this.getWidth(), this.getHeight(), null);
      } 
      catch (IOException e) {
         surface.drawString("Image inexistante", 10, 10);
      }      
   }   
}

Beaucoup de choses sont à dire quant aux traitements des images, notamment sur le reéchantillonage. Nous pouvons largement faire plus performant en choisissant, par exemple, le type d'algorithme de reéchantillonage. Toute une étude sera consacrée aux traitements des images. Toutefois, nous allons quand même parler de la notion de spectateur.

 

Spectateurs

Les images sont traitées de façon asynchrone, ce qui signifie que Java effectue les opérations telles que les charagement et le dimensionnement des images de son côté (permettant au code principal de continuer). Dans une application cliente classique ceci n'est pas très important ; les images sont en général petites, pour les boutons par exemple, et le plus souvent assemblées avec l'application pour un affichage instantané. Toutefois, comme vous le constaterez dans les API de manipulation de données d'images, Java a été conçu pour manipuler des images à la fois localement et sur le Web.

En fait, s'il s'agit d'une nouvelle image, Java n'essaiera même pas de la transférer tant qu'elle ne sera pas nécessaire. L'avantage de cette technique, c'est que Java peut effectuer le traitement des images dans un environnement puissant et multi-thread. Toutefois, cela pose certains problèmes.

Lorsque Java récupère une image, comment savoir si elle est entièrement chargée ? Que se passe-t-il si nous avons besoin de connaître les propriétés de l'iamge (comme ses dimensions) avant de pouvoir commencer à travailler avec ? Que se passe-t-il si une erreur se produit lors du chargement de l'image ?

Ces problèmes sont traités par les spectateurs, des objets qui implémentent l'interface ImageObserver. Toutes les opérations qui dessinent ou traitent des objets Image reviennent immédiatement, mais prennent un objet spectateur en paramètre. Les objets ImageObserver surveillent l'état de l'image et mettent ces informations à la disposition du reste de l'application. Lorsque les données de l'image sont chargées, un spectateur est informé de la progression. Il est averti de la disponibilté de nouveaux pixels, de l'achèvement d'une zone de l'image et de la survenue d'une erreur en cours de chargement. Le spectateur reçoit dès que possible certains attributs sur l'image tels que la dimensions et d'autres propriétés.

La méthode drawImage(), comme d'autres opérations sur les images (getWidth() et getHeight() de Image notamment), prend une référence sur un objet ImageObserver en paramètre. Cette méthode renvoie une valeur booléenne qui indique si l'image a été (ou pas) affichée entièrement. Si l'image n'a pas encore été téléchargée ou si elle n'est qu'en partie disponible, la méthode drawImage() ne dessine qu'une fraction de l'image et se termine.

En arrière-plan, le système graphique commence (ou continue) à charger les données de l'image. L'objet spectateur est enregistré comme étant intéressé par les informations concernant cette image. Il est donc appelé de façon répétée (périodiquement) lorsque les données supplémentaires sont reçues et lorsque l'image entière est arrivée.

Le spectateur peut faire ce qu'il veut de ces informations. Le plus souvent, il appelle la méthode repaint() pour ordonner au composant graphique contenant l'image de dessiner la zone de l'image fraîchement arrivée.

N'oubliez pas que la méthode repaint() entraîne un appel à la méthode paintComponent(), là où nous plaçons notre code spécifique correspondant à notre affichage personnalisé. De cette manière, le composant graphique peut redessiner l'image à mesure qu'elle arrive ou attendre son chargement complet.

Il existe des spectateurs préfabriqués. Il se trouve précisément dans tous les composants graphiques, comme JComponent. En effet, JComponent implémente l'interface ImageObserver et fournit une fonction de rafraîchissement simple. Cela signifie que tout composant graphique peut être utilisé comme son propre spectateur : nous ne faisons, dans ce cas là, que faire passer une référence à notre propre objet graphique à l'aide de this. Voici ce que nous pouvons écrire sur l'un des exemples traité précédemment (la partie modifiée est en noir) :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         Image image = ImageIO.read(new File("chouette.jpg"));
         double positionX = (this.getWidth()-image.getWidth(this)) / 2;
         double positionY = (this.getHeight()-image.getHeight(this)) / 2;
         surface.drawImage(image, (int)positionX, (int)positionY, this);
      } 
      catch (IOException e) {
         surface.drawString("Image inexistante", 10, 10);
      }     
   }   
}




Dans ce cas là, notre composant Zone est utilisé comme spectateur et appele la méthode repaint() pour redessiner l'image si nécessaire. Si l'image arrive lentement, le composant Zone est averti régulièrement à l'arrivée de nouvelles données. L'image est donc affichée progressivement.

Les propriétés awt.image.incrementaldraw et awt.image.redrawarate contrôle ce comportement. redrawate limite le nombre d'appels à la méthode repaint() : la valeur par défaut est une fois toutes les 100 millisecondes. incrementaldraw est la valeur par défaut. Elle est initialisée à true. Il suffit de la positionner à false pour différer le tracé de l'image.

Dans mes différents exemples, je n'est pas mis en oeuvre de spectateur, puisque l'image est petite et de plus je suis sûr qu'elle est présente sur le disque dur. Dans ce cas là vous spécifiez la valeur null au paramètre spectateur.

 

Choix du chapitreTransformation de coordonnées

Souvenez-vous que quatre parties du pipeline d'affichage affectent chaque opération graphique. En particulier, tout le rendu est transformé, composé et masqué. Supposons que vous deviez dessiner un objet, par exemple une voiture. Vous connaissez, grâce aux indications du constructeur, la hauteur, l'empattement et la longueur totale de cette voiture. Vous pouvez naturellement déterminer la position de tous les pixels, en choisissant une échelle en nombre de pixels par mètre. Cependant, il existe une technique plus simple. Vous pouvez demander au contexte graphique d'effectuer une conversion à votre place :

surface.scale(pixels par mètre, pixels par mètre);
surface
.draw(new Rectangle2D.Double(coordonnées et dimensions en mètres)));

La méthode scale() de la classe Graphics2D définit la transformation de coordonnées du contexte graphique et choisit une transformation d'échelle. Cette transformation permet de passer de coordonnées de l'utilisateur (c'est-à-dire les unités spécifiées par l'utilisateur) à des coordonnées machine (c'est-à-dire des pixels).

 

Les transformations de coordonnées sont très utiles dans la pratique. Elles vous permettent de travailler avec des valeurs de coordonnées plus simple et plus significatives. Elles correspondent à votre logique métier, avec celles dont vous avez l'habitude de travailler. Le contexte graphique s'occupe ensuite de les transformeren pixels ( à votre place).

 

Types de transformation

Il existe quatre transformations fondamentales :

  1. Changement d'échelle - scale(): réduit ou augmente toutes les distances à un point spécifié.
  2. Rotation - rotate() : Tourne tous les points autour d'un point fixe.
  3. Translation - translate(): Déplace tous les points d'une quantité donnée.
  4. Déformation linéaire - shear() : Une ligne reste fixe et toutes les lignes parallèles sont décalées d'une quantité proportionnelle à la distance entre cette ligne et la ligne fixe.

Les méthodes scale(), rotate(), translate() et shear() de la classe Graphics2D choisissent une transformation de coordonnées pour le contexte graphique, en fonction de l'une de ces quatre transformations fondamentales.

 

Composer les transformations

Il est tout à fait possible de composer ces transformations. Par exemple, vous pouvez tourner les formes et doubler leur taille. Il vous faut alors passer à la fois par une rotation et un changement d'échelle :

surface.rotate(angle);
surface.scale(2, 2);
surface
.draw(...);

Dans ce cas, l'ordre des transformations n'a aucune importance. Cependant, avec d'autres transformations, l'ordre des compositions peut avoir de l'importance. Par exemple, si vous souhaitez appliquer une rotation et une déformation, vous devez bien réfléchir pour savoir quelle transformation appliquer en premier. Le contexte graphique appliquera ces transformations dans l'ordre dans où vous les lui fournissez.

Vous pouvez fournir autant de transformations que vous désirez.

Comme une rotation autour d'un autre point que l'origine est une opération très courante, il existe un racourci :

surface.rotate(angle, x, y);

Transformations temporaires - description de la classe AffineTransform

Si vous souhaitez uniquement appliquer une transformation de manière temporaire, il convient de récupérer l'ancienne transformation, de la composer avec la nouvelle transformation et de restaurer l'ancienne transformation lorsque vous avez fini. Pour stocker une transformation temporaire, vous devez passer par la classe AffineTransform qui représente n'importe quel type de transformation. Ensuite, pour récupérer une transformation, utilisez la méthode getTransform() de la classe Graphics2D. Enfin, pour redonner l'ancienne transformation, prenez la méthode setTransform() de Graphics2D.

AffineTransform ancienneTransformation =surface.getTransform();
// enregistre l'ancienne transformation
surface.rotate(angle, x, y);
// enregistre l'ancienne transformation
surface.setTransform(ancienneTransformation);
// restitue l'ancienne transformation

La classe AffineTransform peut être utile pour d'autres objets, comme par exemple pour l'objet TextLayout que nous évoquerons plus loin. En effet, cette classe dispose de méthodes équivalentes aux méthodes de transformation qui existent dans la classe Graphics2D. Ainsi, lorsque nous avons besoin d'un objet qui représente plus spécifiquement la transformation voulue, il suffit de faire appel à la bonne méthode statique. En fait, il existe les méthodes de construction : getRotateInstance(), getScaleInstance(), getTranlateInstance() et getShearInstance() qui construise des objets de transformation relatif aux translations requises.

AffineTransform transformation =AffineTransform.getTranslateInstance(10, 250);

Une fois qu'un objet AffineTransform est construit, il est possible de rajouter d'autres transformations à l'aide de ses méthodes internes déjà connues : rotate(), scale(), translate(), et shear().

transformation.rotate(Math.PI/2);

 

Exemple de synthèse

Voici un exemple qui regroupe le tracé de formes quelconques, de texte et d'image que nous avons élaborés au fur et à mesure pour bien montrer que les transformations s'applique quelque soit le type de tracé. Au préalable, voici le code correspondant, mais sans aucune transformation :


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         // tracé de l'image
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 10, 10, this.getWidth()/2, this.getHeight()/2, null);
          
         // tracé du pétale de fleur
         CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);
         surface.setPaint(Color.BLUE);
         surface.fill(pétale);
         Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60);
         surface.setStroke(new BasicStroke(10));
         surface.draw(cercle);
         surface.setPaint(new Color(255, 128, 0));
         surface.fill(cercle);
        
         // tracé du message de bienvenue
         surface.setPaint(new GradientPaint(10, 10, Color.red, 30, 30, Color.yellow, true));
         String message = "Bienvenue !";
         Font police = this.getFont().deriveFont(48F);
         surface.setFont(police);
         surface.drawString(message, 20, 80);            
      } 
      catch (IOException e) { 
         surface.drawString("Image inexistante", 10, 10); 
      }
   }   
}

Cette fois-ci nous allons appliquer des transformations différentes sur chacun des tracé :

  1. Pour le tracé de l'image, je propose une inclinaison vers la droite. Pour cela j'utilise la méthode shear(). Attention, les arguments de cette méthode correspondent à une pente. Ainsi, si nous plaçons la valeur 1, cela correspond à la droite y=x, c'est-à-dire à une pente en degré de 45°.
  2. Pour le tracé des formes quelconques, je propose une rotation du pétale de 45°. Attention, il faut faire une rotation par rapport au centre du cercle. Par ailleurs, l'angle s'exprime en radian. De plus, le sens de rotation correspond au sens horaire. Pour cela, j'utilise la méthode rotate() avec les trois paramètres.
  3. Pour le tracé du texte, je propose une inclinaison à la fois en x et en y. Je réutilise de nouveau la méthode shear() où cette fois-ci je règle, à la fois la valeur de transformation dans l'axe des x et la valeur de transformation dans l'axe des y.

class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      AffineTransform transformation = surface.getTransform();  <===============
      try {
         // tracé de l'image avec une rotation de 90°
         surface.shear(-0.5, 0);   <=====================================
         surface.translate(70, 0);  <====================================
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 10, 10, this.getWidth()/2, this.getHeight()/2, null);
         surface.setTransform(transformation); <===========================
          
         // tracé du pétale de fleur
         surface.rotate(-Math.PI/4, 150, 170);  <===========================
         CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170);
         surface.setPaint(Color.BLUE);
         surface.fill(pétale);
         Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60);
         surface.setStroke(new BasicStroke(10));
         surface.draw(cercle);
         surface.setPaint(new Color(255, 128, 0));
         surface.fill(cercle);
         surface.setTransform(transformation); <===========================
        
         // tracé du message de bienvenue
         surface.shear(0.5, 0.4);  <====================================
         surface.translate(-20, -20); <==================================
         surface.setPaint(new GradientPaint(10, 10, Color.red, 30, 30, Color.yellow, true));
         String message = "Bienvenue !";
         Font police = this.getFont().deriveFont(48F);
         surface.setFont(police);
         surface.drawString(message, 20, 80);            
      } 
      catch (IOException e) { 
         surface.drawString("Image inexistante", 10, 10); 
      }
   }   
}

 

Choix du chapitre Clipping

En définissant une forme de clipping dans le contexte graphique, vous restreignez toutes les opérations graphiques à l'intérieur de cette forme. Cela correspond à la mise en oeuvre d'un masque dans les logiciels de traitement d'images.

surface.setClip(forme du masque);
surface
.draw(forme quelconque);
// affiche uniquement la partie de la forme qui se trouve à l'intérieur du masque.

Cependant, dans la pratique, vous n'appellerez pas explicitement la méthode setClip() puisqu'elle remplace le masque que le contexte graphique possède éventuellement. Par exemple, un contexte graphique d'impression contient un masque sous forme de rectangle qui empêche d'imprimer sur les marges de la feuille. Il vaut mieux, à la place, appeler la méthode clip(). Dans ce cas là, c'est juste la forme qui est passée en paramètre qui sert de masque temporaire.

surface.clip(forme du masque);
// la méthode clip() réalise l'intersection du masque existant avec la nouvelle forme de masque que vous passez en paramètre.

Fabriquer un masque à partir d'un texte

Nous allons fabriquer un programme qui met en oeuvre les masques. La particularité de ce programme, c'est que c'est un texte qui sert de masque. Nous demanderons ensuite à afficher une image au travers de ce masque. L'astuce consiste à utiliser un gestionnaire de disposition particulier qui permet de placer des éléments quelconques dans la surface délimité par un texte. Ce gestionnaire est représenté par la classe TextLayout.

Pour créer un objet correspondant à ce gestionnaire de disposition TextLayout, vous devez fournir :

  1. La chaîne de caractère.
  2. La police utilisée.
  3. Le contexte d'affichage de police.

TextLayout calque =new TextLayout("Notre texte" , police, contexte);

Cet objet de mise en page texte décrit la disposition d'une séquence de caractères, affichés par un contexte d'affichage de police particulier. La mise en page dépend du contexte d'affichage de police : les mêmes caractères apparaîtrons différemment sur un écran ou sur une imprimante.

Cette classe TextLayout possède une méthode importante, getOutLine() qui délivre un objet Shape qui lui-même décrit la forme du contour des caractères, dans la mise en page de texte retenu. La forme du contour commence à l'origine (0, 0), ce qui n'est pas très pratique pour la plupart des opérations de dessin. Par conséquent, il faut fournir une transformation affine, au moyen de la classe AffineTransform, pour l'opération getOutLine(), qui spécifie la nouvelle position du contour.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      // mise en oeuvre du masque
      AffineTransform transformation = AffineTransform.getTranslateInstance(10, 170);
      transformation.scale(1, 3);
      Font police = new Font("SansSerif", Font.ITALIC+Font.BOLD, 60);
      FontRenderContext contexte = surface.getFontRenderContext();     
      TextLayout calque = new TextLayout("Bonjour !", police, contexte);
      Shape masque = calque.getOutline(transformation);
      surface.clip(masque);
      try {
         // tracer une image dans le masque
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 0, 0, null);
      } 
      catch (IOException e) { 
         surface.drawString("Image inexistante", 10, 10); 
      }
   }   
}

 

Choix du chapitre Transparence et composition

Avec le modèle de couleurs standard RVB, chaque couleur est décrite par ses composantes de rouge, de vert et de bleu. Cependant, il peut être également intéressant de décrire des zones de l'image qui sont transparentes, ou partiellement transparentes. Lorsque vous supperposez une image et un dessin existant, les pixels transparents ne modifient pas les pixels existants, alors que des pixels partiellement transparents sont mélangés avec des pixels existants.

L'exemple ci-dessous montre le résultat d'une superposition d'un texte partiellement transparent sur une image. Les détails de l'image restent visibles sous le texte.


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      try {
         // tracer l'image de fond
         Image image = ImageIO.read(new File("chouette.jpg"));
         surface.drawImage(image, 0, 0, null);
      } 
      catch (IOException e) { 
         surface.drawString("Image inexistante", 10, 10); 
      }
       // tracé du texte en transparence sur l'image
      surface.setPaint(new Color(0, 0, 255, 64));
      surface.setFont(new Font("SansSerif", Font.ITALIC+Font.BOLD, 60));
      surface.drawString("Chouette", 10, 130);       
   }   
}

Avec L'API Java 2D, la transparence est décrite par une couche alpha. Chaque pixel possède, en plus de ses composantes de rouge, de vert et de bleu, une valeur alpha variant entre 0 - transparence parfaite - et 255 - opacité (dans le cas où les paramètres sont de type int). Pour cela, il suffit d'utiliser le constructeur de la classe Color qui intègre la couche alpha :

Color(int rouge, int vert, int bleu, int alpha); // valeur des paramètres varie de 0 à 255

En reprenant l'exemple, le réglage de la couleur du texte se fait de la façon suivante :

surface.setPaint(new Color(0, 0, 255, 64));

Il existe un deuxième contructeur, dont le fonctionnement est plus intuitif, qui prend cette fois-ci des float en paramètres. Dans ce cas là, il faut spécifier la valeur de chacune des couches par un pourcentage :

Color(float rouge, float vert, float bleu, float alpha); // valeur des paramètres varie de 0F à 1F

En reprenant l'exemple, le réglage de la couleur du texte en intégrant plutôt la notion de pourcentage est la suivante :

surface.setPaint(new Color(0F, 0F, 1F, 0.25F));

 

Règles de composition pour la transparence

Examinons maintenant ce qui se passe lorsque vous supperposez deux formes. Vous devez mélanger ou composer les couleurs et les valeurs alpha des pixels sources et destinations. Il existe douze règles de composition possibles pour ce traitement.

Si vous trouvez que ces règles sont obscures et mystérieuses, il vous suffit de choisir la règle SRC_OVER. Il s'agit de la règle par défaut pour les objets Graphics2D et c'est celle qui fournit les résultats les plus intuitifs.

En fait, ces règles permettent de spécifier une préférence plus marquée soit sur la source ou soit sur la destination :

CLEAR
SRC
DST
SRC_OVER
DST_OVER
SRC_IN
SRC_OUT
DST_IN
DST_OUT
SRC_ATOP
DST_ATOP
XOR
La source efface la destination.
La source remplace la destination et les pixels vides.
La source n'affecte pas la destination.
La source est mélangée à la destination et remplace les pixels vides.
La source n'affecte pas la destination et remplace les pixels vides.
La source remplace la destination.
La source efface la destination et remplace les pixels vides.
L'alpha de la source modifie la destination.
Le complément de l'alpha de la source modifie la destination.
La source se mélange à la destination.
L'alpha de la source modifie la destination. La source remplace les pixels vides.
Le complément de l'alpha de la source modifie la destination. La source remplace les pixels vides.

 

Mise en oeuvre de la composition

La méthode setComposite() de la classe Graphics2D sert à installer un objet d'une classe qui implémente l'interface Composite. L'API Java2D fournit une classe de ce type, AlphaComposite, qui implémente les règles que nous venons de décrire.

La méthode de production getInstance() de la classe AlphaComposite fournit un objet AlphaComposite. Vous devez alors fournir la règle et la valeur alpha à utiliser pour les pixels source :

int règle = AlphaComposite.SRC_OVER;
float alpha = 0.25F ;
surface.setComposite(AlphaComposite.getInstance(règle, alpha));
surface.setPaint(Color.blue);
surface.drawString("Chouette", 10, 130);

Voici quelques exemples de l'exemple précédent suivant la règle de composition :

AlphaComposite.CLEAR
AlphaComposite.SRC_OUT

AlphaComposite.DST
AlphaComposite.DST_OVER
AlphaComposite.DST_IN
AlphaComposite.DST_OUT
AlphaComposite.DST_ATOP
AlphaComposite.XOR
AlphaComposite.SRC
AlphaComposite.SRC_IN
AlphaComposite.SRC_OVER
AlphaComposite.SRC_ATOP

 

Choix du chapitre Conseil d'affichage

Dans les sections précédentes, vous avez pu vous rendre compte que le processus d'affichage est assez complexe. Alors que l'API Java 2D est en général étonnament rapide, il se peut que vous désiriez dans certain cas un contrôle plus précis sur les compromis entre la vitesse d'affichage et la qualité d'affichage. La méthode setRenderingHint() de la classe Graphics2D vous permet de définir un seul conseil. Les clés et les valeurs des conseils sont déclarées dans la classe RenderingHints.

surface.setRenderingHint(clé, valeur);

Clé Valeurs Explications
KEY_ANTIALIASING VALUE_ANTIALIAS_ON,
VALUE_ANTIALIAS_OFF,
VALUE_ANTIALIAS_DEFAULT
Active ou désactive l'anticrénelage des formes.
KEY_RENDERING VALUE_RENDER_QUALITY,
VALUE_RENDER_SPEED,
VALUE_RENDER_DEFAULT
Lorsque c'est possible, sélectionne des algorithmes d'affichage pour un meilleur résultat en termes de qualités ou de vitesse.
KEY_DITHERING VALUE_DITHER_ENABLE,
VALUE_DITHER_DISABLE,
VALUE_DITHER_DEFAULT
Active ou désactive la diffusion (dithering) des couleurs. La diffusion simule un nombre plus important de couleurs en créant des motifs de couleurs proches.
KEY_TEXT_ANTIALIASING VALUE_TEXT_ANTIALIAS_ON,
VALUE_TEXT_ANTIALIAS_OFF,
VALUE_TEXT_ANTIALIAS_DEFAULT
Active ou désactive l'anticrénelage des polices.
KEY_FRACTIONALMETRICS VALUE_FRACTIONALMETRICS_ON,
VALUE_FRACTIONALMETRICS_OFF,
VALUE_FRACTIONALMETRICS_DEFAULT
Active ou désactive le calcul des dimensions fractionnelles des caractères. Les dimensions fractionnelles des caractères permettent de les positionner plus précisément.
KEY_ALPHA_INTERPOLATION VALUE_ALPHA_INTERPOLATION_QUALITY,
VALUE_ALPHA_INTERPOLATION_SPEED,
VALUE_ALPHA_INTERPOLATION_DEFAULT
Active ou désactive le calcul précis des composantes alpha.
KEY_COLOR_RENDERING VALUE_COLOR_RENDER_QUALITY,
VALUE_COLOR_RENDER_SPEED,
VALUE_COLOR_RENDER_DEFAULT
Choisit la qualité ou la vitesse pour l'affichage des couleurs.
KEY_INTERPOLATION VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
VALUE_INTERPOLATION_BILINEAR,
VALUE_INTERPOLATION_BICUBIC
Sélectionne une règle pour interpoler les pixels lorsque les images sont déformées.
KEY_STROKE_CONTROL VALUE_STROKE_NORMALYZE,
VALUE_STROKE_PURE,
VALUE_STROKE_DEFAULT
Sélectionne une règle pour la combinaison des traits.

 

L'anticrénelage : anti-aliasing

L'aspect le plus intéressant est la gestion de l'anticrénelage (en anglais : anti-aliasing). Il s'agit d'une technique permettant de réduire l'effet d'escalier des lignes inclinées et des courbes.

Sans anticrénelage

Avec anticrénelage

Comme vous pouvez le constater, les formes et les textes sont affichés avec un effet de marches d'escalier dépendant de l'angle de la pente à chaque point de la courbe. Cet effet est très peu esthétique, notamment pour les basses résolutions. En revanche, si au lieu de dessiner des points noirs ou blancs, vous calculez une valeur intermédiaire pour tous les points voisins de la courbe à tracer, et que cette valeur intermédiaire soit proportionnelle à la surface du pixel couverte par la courbe, le résultat est bien plus précis. Cette technique est appelée anticrénelage. Elle nécessite bien sûr plus de temps de calcul puisqu'il faut calculer toutes ces valeurs intermédiaires.

Voici comment demander un filtre anitcrénelage :

surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

Il peut également être utile d'appliquer un anticrénelage aux polices de caractères uniquement :

surface.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

Les autres conseils d'affichage ne sont pas utilisés aussi souvent.
.

Exemple de programme sur l'anticrénelage global


class Zone extends JComponent {  
   protected void paintComponent(Graphics g) {
      Graphics2D surface = (Graphics2D) g;
      // règles de rendu
      surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
       // dessin du pétale
      CubicCurve2D pétale = new CubicCurve2D.Double(150, 130, 10, -20, 290, -20, 150, 130);
      surface.setStroke(new BasicStroke(5));
      surface.draw(pétale); 
      // dessin du texte
      surface.setFont(new Font("SansSerif", Font.BOLD+Font.ITALIC, 60));
      surface.drawString("Bonjour !", 10, 190);
   }   
}