Ensemble de Mandelbrot

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

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, de même que les propriétés width et height de la classe Mandelbrot.

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é.

En lisant la méthode scaledBy de Rectangle, on constate que le rectangle qu'elle retourne a le même coin bas-gauche que le récepteur, et que seules les dimensions des côtés diffèrent. Cela implique que pour préserver la position du point P lors d'un zoom, on peut effectuer les opérations suivantes :

  1. translater le cadre de l'image afin de faire coïncider son coin bas-gauche avec le point P,
  2. redimensionner le cadre en fonction du zoom demandé, c-à-d d'un facteur 2 pour un zoom arrière, ½ pour un zoom avant,
  3. inverser la translation mais en tenant compte du facteur de zoom.

La translation initiale à effectuer est donnée par le vecteur noté \(\vec{v}\) dans la figure plus haut, dont les composantes doivent être exprimées dans le repère du plan. On peut les déterminer en fonction des coordonnées du point P dans le repère de l'image, auxquelles on applique une mise à l'échelle. De plus, la coordonnée imaginaire doit être calculée en tenant compte du fait que les coordonnées de P dans le repère de l'image sont exprimées à partir du coin haut-gauche, alors qu'on désire ici faire partir le vecteur du coin bas-gauche.

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 imageToPlane = frame.width() / mandelbrot.width();
    double vRe = e.getX() * imageToPlane;
    double vIm = frame.height() - e.getY() * imageToPlane;
    double scale = (e.isControlDown() ? 2 : 0.5);
    Rectangle newFrame = frame
      .translatedBy(vRe, vIm)
      .scaledBy(scale)
      .translatedBy(-vRe * scale, -vIm * 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 vRe et vIm calculées à l'exercice précédent.

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

imageView.setOnMouseClicked(e -> {
    Rectangle frame = mandelbrot.getFrame();
    double imageToPlane = frame.width() / mandelbrot.width();
    double vRe = e.getX() * imageToPlane;
    double vIm = frame.height() - e.getY() * imageToPlane;

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