Introduction à JAVA

Dessiner et écrire avec Java 2D

Les outils de dessin de la classe Graphics sont limités : méthodes drawX() et fillX() pour les traits, rectangles, ovales et polygones et une zone rectangulaire de clipping. Les traits sont toujours continus et d'épaisseur 1 pixel. Seules les coordonnées entières sont autorisées.

Java 2D offre des multiples possibilités telles que :

  • Dessiner des lignes, rectangles et toute autre forme géométrique avec des traits d'épaisseur réglable et avec des coordonnées réelles (float ou double) ou entières.
  • Remplir les formes avec des couleurs unies, des dégradés ou des textures.
  • Appliquer des opérations des transformations affines (translation, rotations ...)
  • Dessiner des images en appliquant des opérations de filtrage.
  • Dessiner des textes inclinés.
  • Agir sur le rendu (anticrénelage, la couleur et l'interpolation).

On trouvera une étude détaillée des possibilités de Java 2D dans la page JAVA 2D Graphics (The Java™ Tutorials)


Principe général

Pour pouvoir utiliser correctement Java 2D, il faut obligatoirement travailler avec un double buffer.
On crée une image et un contexte graphique avec :
Image ima; Graphics h;
Puis dans init(), on précise ima=createImage(largeur,hauteur); h=ima.getGraphics();
Pour utiliser les méthodes de Java 2D, il faut dire que le contexte graphique utilisé est de type 2D avec
Graphics2D g2=(Graphics2D)h;
On dessine dans h puis dans la méthode public void paint(Graphics g), on provoque l'affichage de h avec g.drawImage(ima,0,0,this);


Les traits et les rendus

Le tracé des traits est conditionné par la classe BasicStoke dont le constructeur le plus général est :

BasicStroke(float width,int cap,int join,float miterlimit,float[ ] dash,float dash_phase);

width est la largeur de ligne est l'épaisseur de la ligne mesurée perpendiculairement à sa trajectoire. La largeur de ligne est spécifiée par valeur en unités de coordonnées utilisateur.
cap est une constante dont la valeur (CAP_BUTT=0, CAP_ROUND=1 ou CAP_SQUARE=2) précise la forme de la fin de la ligne.
join est une constante dont la valeur (JOIN_ROUND=1, JOIN_BEVEL=2, or JOIN_MITER=0) précise la forme de la jonction entre deux lignes d'un objet.
miterlimit est la valeur limite de la longueur de la terminaison des traits en mode JOIN_MITER (en pointe de flèche).
dash est un tableau de type float qui indique les longueurs successives des parties opaques et transparentes.
dash_phase est un float qui précise ou commence le tracé.
Exemples :
float motif []= {10.0f,2.0f,2f,2f};
BasicStroke point=new BasicStroke(1.0f,0,0,5.0f,motif,0.0f);

BasicStroke normal=new BasicStroke();

On peut aussi utiliser BasicStroke(float width, int cap, int join, float miterlimit), BasicStroke(float width, int cap, int join) BasicStroke(float width) et enfin BasicStroke() qui donne le tracé par défaut : trait continu avec les valeurs (1.0f, CAP_SQUARE, JOIN_MITER, 10.0f).

On définit le type de tracé par g2.setStroke(point); et on trace dans le contexte graphique h;

L'applet ci-dessous montre l'influence des divers paramètres. Cliquer pour avoir le code source

En mode pointillés, on constate qu'il faut prendre cap = CAP_BUTT.

Il est possible d'améliorer (au dépend de la rapidité d'exécution) l'aspect des dessins en utilisant des "Hints" (conseils) à partir de la classe RenderingHint.
Le plus utile est l'anticrénelage (anti-aliasing) qui améliore nettement l'aspect des traits presque verticaux ou presque horizontaux. Il est possible d'utiliser plusieurs "clés" en précisant clés et valeurs.

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);


Les Formes

Il est toujours possible d'utiliser les méthodes drawLine(x1, y1, x2, y2), drawRect(x1 ,y1, large, haut), drawRoundRect(x1, y1, large, haut, R1, R2), drawOval(x1, y1, R1, R2), drawArc(x1, y1, R1, R2, debut, long) et drawPolygon(int[ ]X, int[ ]Y, int n) qui utilisent toutes des arguments entiers.

Si l'on désire utiliser des arguments de type Double, il faut utiliser les méthodes :
Line2D.Double(x1, y1, x2, y2), Rectangle2D.Double(x1 ,y1, large, haut), RoundRectangle2D.Double(x1 ,y1, large, haut,R1, R2), Ellipse2D.Double(x1, y1, R1, R2) et Arc2D.Double(x1, y1, R1, R2, debut, fin, fermeture).
Pour les arcs, début et long sont les valeurs en degrés du départ de l'arc (0° à 3 heures) et sa longueur. Fermeture est un entier dont la valeur (OPEN, PIE et CHORD) précise le type de fermeture de l'arc.
Les mêmes méthodes existent pour les Float.
Pour effectuer un tracé, on utilise g2.draw(new Line2D.Double(x1, y1, x2, y2));
Pour les polygones, on dispose de nouvelles méthodes dont la plus utile est P.addPoint(int x, int y); qui ajoute au polygone P un sommet de coordonnées x et y.
Pour utiliser ces formes vectorielles, il faut inclure java.awt.geom.*;

Les formes sont dotées des méthodes getBounds(x, y, large, haut) qui retourne le plus petit rectangle contenant la forme, contains(x, y) et contains(x, y, large, haut) qui indiquent si la forme contient le point ou le rectangle et intersects(x, y, large, haut) qui indique si la forme coupe ce rectangle.

L'applet ci-dessous montre quelques exemples de tracé de formes. Cliquer pour avoir le code source


Courbes de Bézier

Java2D permet le tracé automatique des courbes de Bézier :
Cubique définie par l'origine, l'extrémité et deux points de contrôle.
Quadrique définie par l'origine, l'extrémité et un point de contrôle.
Pour la cubique, la courbe obtenue est tangente à la droite définie par l'origine et le premier point de contrôle ainsi qu'à la droite définie par l'extrémité et le second point de contrôle.
Pour la quadrique, la courbe obtenue est tangente aux droites définies par l'origine et le point de contrôle et par l'extrémité et le point de contrôle.

Comme données, on déclare :
CubicCurve2D cub=new CubicCurve2D.Double();
protected Point2D [ ] pt=new Point2D[4];{
pt[0]=new Point2D.Double(50,100); //origine
pt[1]=new Point2D.Double(200,220); //contrôle 1
pt[2]=new Point2D.Double(420,200); //contrôle 2
pt[3]=new Point2D.Double(550,80);} //extrémité
Lors du dessin pour créer la courbe on utilise cub.setCurve(pt[0],pt[1],pt[2],pt[3]);
puis on provoque le tracé par g2.draw(cub);
On peut aussi utiliser directement : cub.setCurve(x1, y1, ctx1, cty1, ctx2,cty2, x2, y2);

Pour la quadrique, on utilise :
QuadCurve2D quad=new QuadCurve2D.Double(); et quad.setCurve(pt[0],pt[1],pt[3]);

L'applet ci-dessous montre le tracé de ces courbes. Avec la souris, il est possible de déplacer les points de contrôle. Cliquer pour avoir le code source


Formes composées (Area)

Il est possible de réaliser des formes complexes par composition de formes simples avec des opérations booléennes.
On déclare les formes à combiner par :
Shape rond=new Ellipse2D.Double(50,50,160,100);
Shape carre=new Rectangle2D.Double(150,90,80,100);
Puis par :
Area A1=new Area(rond);
Area A2=new Area(carre);

Ensuite on combine les formes avec :
A1.add(A2); //A1 devient l'addition de A1 et A2
A1.subtract(A2) //A1 devient A1 diminue de la partie commune à A1 et A2
A1.intersect(A2); //A1 devient la partie commune de A1 et A2
A1.exclusiveOr(A2);
//A1 devient (A1 + A2) diminuée de la partie commune à A1 et A2
On effectue le tracé avec g2.draw(A1) ou g2.fill(A1).

L'applet ci-dessous montre un exemple de tracé de formes complexes avec les surfaces jaunes.
Cliquer pour avoir le code source


Dégradés de couleurs

Il est possible d'utiliser comme couleur de remplissage un dégradé entre deux couleurs précisées en deux points.
On utilise : Paint pt=new GradientPaint(x1 ,y1, Color1, x2, y2, Color2);
Puis : g2.setPaint(pt);
(x1, y1) est le point de départ et Color1 sa couleur. (x2, y2) est le point d'arrivée et Color2 sa couleur.


Compositions de couleurs

Comme constructeurs de Color, on dispose de :
Color(int, int, int), Color(int), Color(int, boolean), Color(float, float, float),
et aussi Color(int, int, int, int), Color(float, float, float, float).
On dispose dans les containers muni d'une couche "alpha" de la transparence.

La classe AlphaComposite permet 12 modes de compositions entre l'image existante et l'image finale.
Le système de composition de Java2D utilise :
As la composante alpha du pixel source, Cs la couleur du pixel source, Ad la composante alpha du pixel de destination, Cd la couleur du pixel destination, Fs la fraction du pixel source contribuant à la sortie, Fd la fraction du pixel destination contribuant à la sortie, Ar la composante alpha du pixel résultat, Cr la couleur pixel résultat.
Par exemple pour le mode SRC_OVER, on a : Ar = As*(1 − Ad) + Ad et Cr = Cs*(1 − Ad) + Cd
et pour le mode XOR, Ar = As*(1 − Ad) + Ad*(1 − As) et Cr = Cs*(1 − Ad) + Cd*(1 − As) .
Attention pour CLEAR Fs= 0 et Fd = 0, pour DST Fs= 0 et Fd = 1. et pour SRC Fs= 1 et Fd = 0.

Les résultats sont difficiles à prévoir sauf pour le mode SRC_OVER.

L'applet ci-dessous montre un exemple d'utilisation. Cliquer pour avoir le code source.
Déplacer l'ellipse avec la souris. Le rectangle jaune et l'ellipse rouge sont dotés de la même règle de composition. Toujours revenir au mode SRC_OVER avant d'examiner un autre mode.


Transformations affines

Les transformations affines permettent de modifier les coordonnées utilisateur avant affichage. Il est ainsi possible de centrer le repère au centre de l'écran. Les transformations possibles sont la translation, la rotation, l'homothétie et le cisaillement. Elles s'appliquent à tous les objets que l'on peut manipuler avec Java2D y compris les chaînes de caractères. Une opération affine dans le plan est représentable par une matrice 3x3 dont la dernière ligne est toujours égale à 0 0 1. Il est possible de modifier explicitement chaque coefficient avec les méthodes setToTranslation(), setToRotation(), setToScale() et setToShear() de la classe AffineTransform. On peut aussi utiliser les méthodes directes :
AffineTransform afftran = new AffineTransform(); //T1
afftran.translate(50,150);
afftran.scale(1.1f,1.0f);
afftran.rotate(alpha);
g2bufima.setTransform(afftran); //application au contexte graphique

Il est possible de redéfinir la transformation au cours des opérations de dessin mais bien sur avant l'écriture dans le tampon final. Celui-ci est toujours disponible pour des opérations de dessin qui ne sont pas affectées par les transformations affines
Ces opérations fonctionnent aussi avec les images mais il faut alors utiliser une "BufferedImage". (voir le code de l'exemple).

. Cliquer pour visualiser l'applet et avoir son code source.