Ailleurs
Java
Démo

Remplissage hachuré

Java2D utilise l'interface Paint pour décrire de façon générale tout ce qui peut servir à remplir une surface ou un trait. Par exemple, la très employée classe Color intègre l'interface Paint. De même, quand on dessine dans un contexte Graphics2D, on utilise la fonction setPaint( Paint p ) pour attribuer une "matière" aux traits ou remplissage qui suivront.

A l'origine, Java2D propose en standard plusieurs classes qui répondent aux besoins de Paint :

Mais pas de contexte pour réaliser des hachures. Cet oubli sera vite réparé. Avant d'aller plus loin, examinons d'abord l'interface Paint (API) :

public interface Paint {
    public int getTransparency();


    public PaintContext createContext(
        ColorModel cm,
Rectangle deviceBounds,
Rectangle2D userBounds,
AffineTransform xform,
RenderingHints hints); }

La méthode getTransparency, qui peut renvoyer java.awt.Transparency.TRANSLUCENT ou java.awt.Transparency.OPAQUE, distingue les contextes qui fabriquent des zones opaques des contextes qui génèrent des zones pouvant contenir des pixels transparents (ce qui sera le cas pour les hachures).

Quant à createContext, elle crée un objet du type PaintContext (API), qui est lui, responsable de remplir la zone impartie avec la matière qu'il représente, couleur, dégradé, ou hachure. En d'autres termes, l'objet du type Paint ne fait pas grand chose, car c'est l'objet du type PaintContext qui fera tout le travail.

public PaintContext {
    public ColorModel getColorModel();
    public Raster getRaster(int x, int y, int w, int h);
    public void dispose();
}

La première méthode sert à obtenir le mode colorimétrique en usage. C'est par son intermédiaire que Java2D saura dans quel modèle le contexte fonctionne. Dans le cas des hachures, il y a fort à parier que le résultat devra correspondre à un modèle couleur (RGB) avec transparence (Alpha).

En seconde position, la méthode getRaster est le coeur du système, c'est elle qui peint dans une zone rectangulaire délimitée par les quatres paramètres. En effet, un PaintContext ne peut remplir que des rectangles. Que se passe-t-il pour remplir une forme bizarre, genre patatoïde ? Facile, Java2D va le tronçonner en rectangles horizontaux, d'épaisseur 1 pixel si nécessaire, avant de les envoyer un par un à cette méthode. Il en découle que pour peindre une seule zone un peu tordue, le recours à getRaster peut être multiple.

La troisième méthode sert à libérer les resources éventuelles utilisées par le contexte. Cela peut s'avérer utile, dans le cas d'un motif image qui encombrerait la RAM. Dans le cas des hachures, les besoins sont limités et cette fonction n'aura que peu d'intérêt.

Ce que doit faire un contexte de hachure

Nous avons à fixer trois paramètres simples pour définir une hachure :

Pour des raisons de simplicité, le contexte ne prend en compte que les traits pleins, pas les pointillés ou autre discontinus. Pour améliorer le rendu, vous pourrez constater que le contexte supporte très bien l'antialiasing, et donne de bons rendus quelque soit l'angle choisi.

Les paramètres sont définis au sein de HatchedPaint (source), avec les getters et setters qui s'imposent. HatchedPaint contient un objet HatchedPaintContext (source) pour réaliser le travail de rendu (par getRaster).

 

Couleur du remplissage

Au sein de la fonction getRaster, qui doit peindre une zone rectangulaire, une double boucle x / y est enclenchée pour examiner chaque pixel, un à un. La question qui doit se poser pour lui est : "Suis-je là où passe un trait, ou non. Dans le premier cas, je dois me peindre dans la couleur démandée, sinon, je reste vierge, pour laisser apparaître le fond existant". Mais la réponse n'est pas aussi tranchée que le laisse sous-entendre ce qui précède. Dans le modèle exposé, cela revient à peindre un point sur le trait, et ne rien faire dans les autres cas. Les hachures qui en résultent ont des bords très durs, avec un effet d'escalier très marqué. Pour améliorer le rendu, nous allons appliquer un procédé d'anti-aliasing.

Si le pixel à déterminer se trouve exactement à l'emplacement d'une hachure, alors il sera peint avec la couleur pleine de celle-ci. Mais si le pixel se trouve à une distance très proche de la hachure, il sera quand même peint, mais avec une nuance plus transparente. L'effet obtenu sera celui de droite, plutôt que celui de gauche.

Il n'existe donc pas une seule couleur de hachure, mais toute une série de nuances plus ou moins transparentes qui sont préparées à l'avance, dans la fonction createAliasedColorTable.

//c est la couleur de la hachure (objet Color)
int cc = c.getRGB();
//récupération de la transparence de cette couleur
float a = (0xFF & (cc>>24))/255f;
//ôte la transparence pour ne garder que la tiente
cc=0xFFFFFF & cc;

colors = new int[aliasMax];
//la table est symétrique avec la couleur opaque au centre
//et des nuances de + en + transparentes à g et à d.
for(int i=0; i<aliasmax/2; i++) {
    int alpha = (int)(i/(aliasMax/2f-1f)*a*255.0f); 
    alpha <<= 24;
    colors[i] = alpha | cc;
    colors[colors.length-i-1] = alpha | cc;
}
//couleur opaque à ajouter au centre du tableau quand celui-ci
//est de taille impaire.
if (aliasMax%2!=0) colors[aliasMax/2]= 0xFF000000 | cc;

Par défaut, la taille de la table des couleurs est fixé à aliaxMax = 25. Cela donne de bons résultats.

Gérer l'angle et l'espacement des hachures

Si on en revient à la question de tout à l'heure, "doit-on peindre le pixel (x,y) ?", la problématique a évolué vers une question "quelle est la distance qui sépare le pixel (x,y) de la plus proche hachure ?", afin de déterminer si on ne peint rien, ou dans le cas contraire, quelle nuance on choisit pour peindre.

La distance doit être mesurée perpendiculairement aux hachures. Pratiquer cette mesure quans les hachures sont verticales ou horizontales est extrêmement simple, mais quand l'angle est quelconque, la tâche est moins aisée, et se révèle assez contre-productive en terme de performances.

Aussi l'idée est de toujours mesurer "à plat". Comment ? En utilisant une transformation du plan qui remet les hachures "verticales", pour revenir à un cas de mesure simple. Cette transformation est évidemment une rotation d'un angle égal à l'inclinaison des hachures. Elle est préparée à l'avance dans le constructeur de la classe, et stockée dans un objet AffineTransform (API).

gat = new AffineTransform();
//décalage de pi/2 pour que l'angle 0 soit horizontal
gat.rotate(Math.PI/2.0+angle);
  

Reste à examiner la fonction getRaster pour en comprendre le contenu :

public Raster getRaster(int x, int y, int w, int h) {
    float xref=0;
    //distance est l'espace en pixels entre l'axe de deux hachures
    float distance = space;
    //...
  

On amorce la double boucle qui examine chaque pixel (xx,yy) :

    double r=0;
    double line;
    for(int yy=0; yy<h; yy++) {
        for(int xx=0; xx<w; xx++) {
  

Puis on applique la transformation affine qui permet d'obtenir les coordonnées de ce point dans un repère où les hachures sont verticales.

    //p1 : coordonnées dans l'image du pixel actuel
    p1.x = x+xx;
    p1.y = y+yy;
    //transformation pour obtenir p2 : coordonnées dans le repère des hachures verticales.
    gat.transform(p1,p2);
    //calcul sur l'axe x, de la distance qui sépare le point de la plus proche hachure verticale
    r = (p2.x-xref)/distance;
    r = r-Math.floor(r);
    line = Math.abs(r*distance);
  

Et pour finir, il ne reste plus qu'à appliquer la nuance convenable, quand elle est à une distance acceptable d'une hachure.

   pixels[off++] = line<lineWidth ?
       colors[(int)(line/lineWidth*aliasMax)] :
       0;
  

Sources

HatchedPaint
HatchedPaintContext

Démo Applet

Vous pouvez aussi faire quelques tests avec l'applet en ligne. Cliquez ici.

Haut | Java | Ailleurs