Ensemble de Mandelbrot
Série 10 – corrigé
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 fixe, qui est celle de l'image affichée, d'où le problème constaté.
La première chose à faire pour que l'image soit redimensionnée est donc de lier les dimensions du nœud imageView
à celles du panneau mainPane
:
imageView.fitWidthProperty() .bind(mainPane.widthProperty()); imageView.fitHeightProperty() .bind(mainPane.heightProperty());
Cela fait, on constate que l'image est bien redimensionnée lorsque la fenêtre l'est, mais elle n'est pas recalculée, et est donc déformée si le rapport largeur/hauteur de la fenêtre change. Pour résoudre cela, il faut encore lier les dimensions de l'image de Mandelbrot produite par le bean à celles du nœud imageView
:
mandelbrot.widthProperty() .bind(imageView.fitWidthProperty()); mandelbrot.heightProperty() .bind(imageView.fitHeightProperty());
Une fois ces deux autres liens ajoutés, le redimensionnement de la fenêtre provoque bien le redimensionnement (par recalcul) de l'image.
A noter qu'il existe encore une solution plus simple, qui 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());
La raison pour laquelle cette seconde solution fonctionne est que le nœud ImageView
a un comportement particulier si ses propriétés fitWidth
et fitHeight
valent 0, ce qui est le cas par défaut. Dans ce cas, il affiche l'image qui lui est fournie à sa taille normale.
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 :
- le repère du plan (complexe), dans lequel se trouve l'ensemble de Mandelbrot ; les propriétés
frameWidth
etframeCenter
de la classeMandelbrot
sont exprimées dans ce repère, - 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).
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 :
- 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, - translater le cadre de l'image afin de faire coïncider son coin bas-gauche avec le point P,
- redimensionner le cadre en fonction du zoom demandé, c-à-d d'un facteur 2 pour un zoom arrière, ½ pour un zoom avant,
- 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 } });