Ensemble de Mandelbrot

CS-108 — Corrigé de la série 9

Introduction

Le code du corrigé vous est fourni dans une archive Zip, qui contient également le code de l'énoncé.

Exercice 1

En examinant le graphe de scène construit par le programme principal fourni, on constate qu'il est très simple et ressemble à ceci :

Scene
└── BorderPane (mainPane)
    └── ImageView (imageView, placée dans la zone CENTER)

Lorsque la fenêtre est redimensionnée, l'instance de Scene à la racine l'est aussi, de même que celle de BorderPane. Le nœud ImageView a quant à lui une taille qui est celle de l'image qu'il affiche, et le problème est que cette dernière ne change pas lorsque la fenêtre est redimensionnée.

Pour résoudre ce problème, la solution la plus simple consiste à lier directement les dimensions de l'image de Mandelbrot produite par le bean à celles du panneau mainPane :

mandelbrot.widthProperty().bind(mainPane.widthProperty());
mandelbrot.heightProperty().bind(mainPane.heightProperty());

Exercice 2

Le point crucial à comprendre pour cet exercice est que deux repères différents sont utilisés par différentes parties du code :

  1. le repère du plan (complexe), dans lequel se trouve l'ensemble de Mandelbrot ; les propriétés frameWidth et frameCenter de la classe Mandelbrot sont exprimées dans ce repère,
  2. le repère de l'image ; la position du pointeur de la souris est exprimée dans ce repère.

L'origine et l'orientation des axes de ces deux repères sont présentées dans la figure ci-dessous : le repère du plan est en noir, celui de l'image en rouge. La zone du plan pour laquelle l'image est générée est représentée par le rectangle noir, spécifié par son centre (frameCenter dans le code fc dans la figure) et sa largeur (frameWidth dans le code, fw dans la figure).

frames;16.png
Figure 1 : Repère du plan (en noir) et de l'image (en rouge)

Cette figure montre aussi un point P situé dans l'image, et que l'on suppose être celui sur lequel l'utilisateur a cliqué. Lorsqu'il est fourni au gestionnaire d'événements (via les méthodes getX et getY), il est exprimé dans le repère de l'image. Il faut donc le convertir dans le repère du plan, puis effectuer les opérations suivantes pour zoomer en préservant sa position relative dans le cadre :

  1. calculer la position (x, y) du point P par rapport au coin bas-gauche du cadre de l'image de Mandelbrot, exprimée dans le repère du plan,
  2. translater le cadre de l'image afin de faire coïncider son coin bas-gauche avec le point P,
  3. redimensionner le cadre en fonction du zoom demandé, c-à-d d'un facteur 2 pour un zoom arrière, ½ pour un zoom avant,
  4. inverser la translation mais en tenant compte du facteur de zoom.

Notez que le code ci-dessous inclut déjà une partie du comportement qui était demandé pour l'exercice 3, à savoir le zoom arrière lorsque la touche contrôle est pressée.

imageView.setOnMouseClicked(e -> {
    if (! (e.getClickCount() == 2
	   && e.getButton() == MouseButton.PRIMARY))
      return;

    Rectangle frame = mandelbrot.getFrame();
    double iToP = frame.width() / mandelbrot.getWidth();
    double x = e.getX() * iToP;
    double y = frame.height() - e.getY() * iToP;
    double scale = (e.isControlDown() ? 2 : 0.5);
    Rectangle newFrame = frame
      .translatedBy(x, y)
      .scaledBy(scale)
      .translatedBy(-x * scale, -y * scale);

    mandelbrot.setFrameCenter(newFrame.center());
    mandelbrot.setFrameWidth(newFrame.width());
  });

Exercice 3

Le recentrage peut se faire p.ex. en calculant la nouvelle position du centre du cadre, en ajoutant au coin bas-gauche les valeurs x et y calculées à l'exercice précédent.

En combinant ce nouveau code avec l'existant, et en factorisant le calcul des valeurs communes (frame, x et y), on obtient la version finale du gestionnaire d'événements :

imageView.setOnMouseClicked(e -> {
    Rectangle frame = mandelbrot.getFrame();
    double iToP = frame.width() / mandelbrot.getWidth();
    double x = e.getX() * iToP;
    double y = frame.height() - e.getY() * iToP;

    if (e.getClickCount() == 1
	&& e.getButton() == MouseButton.SECONDARY) {
      // Recenter
      double cx = x + frame.minX();
      double cy = y + frame.minY();
      mandelbrot.setFrameCenter(new Point(cx, cy));
    } else if (e.getClickCount() == 2
	       && e.getButton() == MouseButton.PRIMARY) {
      // Zoom in (w/o control) or out (w/ control)
      // … comme avant
    }
  });