Série 8 – Visualiseur de Mandelbrot : corrigé

Introduction

Le code du corrigé est fourni sous la forme d'une archive Zip, et les solutions aux différents exercices sont rapidement présentées ci-dessous.

Exercice 1

Le dessin de la zone de l'image continue comprise dans le rectangle imageBounds est relativement simple : il suffit, pour chaque pixel de l'image discrète à dessiner, de calculer le point de l'image continue correspondant (variables x et y ci-dessous) puis d'obtenir sa couleur au moyen de la méthode apply :

protected void paintComponent(Graphics g) {
  BufferedImage discreteI =
    new BufferedImage(getWidth(),
                      getHeight(),
                      BufferedImage.TYPE_INT_RGB);

  double sX = imageBounds.width() / getWidth();
  double sY = imageBounds.height() / getHeight();

  for (int dY = 0; dY < getHeight(); ++dY) {
    double y = imageBounds.maxY() - dY * sY;
    for (int dX = 0; dX < getWidth(); ++dX) {
      double x = imageBounds.minX() + dX * sX;
      discreteI.setRGB(dX, dY, image.apply(x, y));
    }
  }

  g.drawImage(discreteI, 0, 0, null);
}

Exercice 2

La définition de l'image de Mandelbrot revient à traduire en Java la formulation mathématique donnée dans l'énoncé. Comme celui-ci le propose, les nombres complexes sont simplement représentés par deux variables de type double, l'une contenant la partie réelle (suffixe r ci-dessous), l'autre la partie imaginaire (suffixe i ci-dessous). Cela implique de « dérouler » les différentes opérations effectuées sur les nombres complexes, de la manière suivante :

  1. le test que le module d'un nombre complexe \(z\) est inférieur à 2 se fait ainsi :
\begin{align*} \|z\| < 2 &\Leftrightarrow \sqrt{z_i^2 + z_r^2} < 2\\ &\Leftrightarrow z_i^2 + z_r^2 < 4 \end{align*}
  1. l'élévation au carré d'un nombre complexe \(z\) se fait ainsi :
\begin{align*} z^2 &= (z_r + i\,z_i)(z_r + i\,z_i)\\ &= z_r^2 - z_i^2 + i\,(2\,z_r\,z_i) \end{align*}
  1. la somme de deux nombres complexes \(y\) et \(z\) se fait ainsi :
\begin{align*} y + z &= (y_r + i\,y_i) + (z_r + i\,z_i)\\ &= y_r + z_r + i\,(y_i + z_i) \end{align*}
public static Image mandelbrot(int maxIterations) {
  return (cr, ci) -> {
    double zr = cr, zi = ci;
    int i = 1;
    while (zr * zr + zi * zi < 4 && i < maxIterations) {
      double zr1 = zr * zr - zi * zi + cr;
      double zi1 = 2d * zr * zi + ci;
      zr = zr1;
      zi = zi1;
      i += 1;
    }

    double p = 1d - Math.pow((double)i / maxIterations,
                             0.25);
    int pI = (int)(p * 255.9999);
    return (pI << 16) | (pI << 8) | pI;
  };
}

Exercice 3

L'auditeur d'événements souris peut se définir comme une classe privée imbriquée statiquement dans le composant ImageComponent. Pour faciliter sa définition, cette classe hérite de MouseAdapter et se contente de redéfinir sa méthode mouseClicked.

Comme le dit l'énoncé, la principale difficulté de cet exercice est de déterminer les paramètres des deux translations à appliquer au rectangle délimitant la zone de l'image affichée.

Pour la première translation, il faut se souvenir du fait que les axes des ordonnées des deux repères (celui de l'image continue et celui du composant) ont une direction opposée.

Pour la seconde translation, qui doit en quelque sorte annuler la première, il faut penser à tenir compte du zoom !

private static final class ImageMouseListener
    extends MouseAdapter {
  private static final double ZOOM = 2;

  private final ImageComponent imageC;

  public ImageMouseListener(ImageComponent imageC) {
    this.imageC = imageC;
  }

  @Override
  public void mouseClicked(MouseEvent e) {
    if (e.getClickCount() != 2)
      return;

    Rectangle imageBounds = imageC.imageBounds();
    double sX = imageBounds.width() / imageC.getWidth();
    double sY = imageBounds.height() / imageC.getHeight();
    int x = e.getX(), y = imageC.getHeight() - e.getY();
    Rectangle newImageBounds = imageBounds
      .translatedBy(x * sX, y * sY)
      .scaledBy(1d / ZOOM, 1 / ZOOM)
      .translatedBy(-x * sX / ZOOM, -y * sY / ZOOM);
    imageC.setImageBounds(newImageBounds);
  }
}

Une fois l'auditeur défini, il convient encore de l'attacher au composant, ce qui se fait simplement dans le constructeur de la classe :

public class ImageComponent extends JComponent {
  public ImageComponent(Image image,
                        Rectangle initialBounds) {
    // … comme avant
    this.addMouseListener(new ImageMouseListener(this));
  }
  // … autres méthodes
}