Programmer l'ensemble de Mandelbrot en javascript
Mettre au point un constructeur d'application

Voir Ensemble de Mandelbrot

1) Introduction

le HTML5, le javascript, le DOM et le CSS, ont l'avantage de s'exécuter tel quel immédiatement sur tous les navigateurs Internet, une portabilité qui surpasse tous les autres langages de programmation. Le manque de rapidité propre aux scripts (langages interprétés) qui constituait un handicap, est résolut avec le temps grace à la loi de Moore qui s'applique toujours (grosso modo un doublement des capacités de mémoire et de rapidité de calcul des ordinateurs tous les 2 ans). Les capacités de calcul des ordinateurs personnels sont aujourd'huis démeusuré et permettent d'exécuter des scripts à la place d'application en langage compilé. L'application javascript constitue un nouveau vecteur de transmission des connaissances, transportant à la fois la théorie et l'algorithme, la théorie constituant l'idée et l'algorithme la mettant en oeuvre constituant un élément de preuve de son caractère véridique.

Néanmoins la programmation d'une application javascript tournant sur les navigateurs Internet reste une activité très laborieuse. L'application comprend trois types de fichier : html, css, js, et qui peut être regroupés dans un unique fichier html. Il est possible de créer un constructeur de tels applications assez simple en abandonnant toute une liberté futile de présentation généralement utilisée pour la publicité. Cela donne un aspect "fiction soviètique de laboratoire" où seul l'aspect scientifique utilitaire apparait dans une simplicité épurée. On se propose de mettre au point dans ce document un tel constructeur d'application.

2) Variable et paramètres

Dans la programmation, le nommage des variables est un choix essentiel qui doit être murement réfléchi pour intégrer une nomenclature lisible, évocatrice, simple, peu verbeuse et pratique. Puis on préfère donner aux variables des noms courts, voir si possible composés d'un seul caractère, comme il est de coutûme dans les équations physique ou mathématique. Car nous voulons programmer dans un état d'esprit davantage mathématicien ou physicien que informaticien.

On choisi qu'un seul type d'élément graphique pour l'entrée/sorties des paramètres, sous forme de tableau à trois colonnes, la première colonne pour les libellés, la seconde colonne pour les noms de variables, et la troisième colonne pour les données. L'affichage des nom de variable est nécessaire pour que le développeur ait continuellement le dictionnaire sous les yeux. Les données sont mises dans des éléments "input" ou dans des éléments "textarea" s'ils tiennent sur plusieurs lignes.

On met en oeuvre quatre façons de percevoir une donnée texte ; soit comme une chaine de caractères tenant sur une ligne (macro µ), ou soit comme une chaine de caractères tenant sur plusieurs lignes (macro µa), ou soit comme un entier (macro µi), ou soit comme un réel (macro µf). Lorsque le constructeur d'application rencontre la macro µ :

µ(Nom ,"Valeur", "Libelle", "Description")

Nom est une chaine de caractères contenant le nom de la variable, où Valeur est optionnel et sera sa valeur par défaut, où Libelle est optionnel et sera son libellé, et où Description est optionnel et sera sa description, il va générer dans le fichier html le code suivant :

<tr>
 <td title"Description"=>Libelle : </td>
 <td class="var">Nom</td>
 <td><input id="Nom" type="text" value="Valeur"></td>
</tr>

et pour la macro µa(Nom, "Valeur", "Libelle", "Description"), il va générer le code suivant :

<tr>
 <td title"Description"=>Libelle : </td>
 <td class="var">Nom</td>
 <td><textarea id="Nom">Valeur</textarea></td>
</tr> 

Et chaque tels entrées doit être regroupées dans un élément "table" simplement en entourant la succession de ces entrées par la balise <table>....</table>. Le code javascript commence par les déclarations de variables avec leur libellé en commentaire. La macro µ va donc également ajouter dans le fichier js le code suivant  :

let Nom  	// Libelle
let $Nom 	// Elément du DOM associé

La phase d'initialisation s'exécute dans la fonction "INIT()". Elle mémorise pour chaque variable son élément "input" ou "textarea" qui lui est associées et qui est mémorisé dans une variable de même nom mais préfixé par le caractère $. Puis elle initialise chaque variable dans la fonction "READ()" et modifie la page web en conséquence dans la fonction "PAGE()". Puis elle lance l'initialisation "INITIAL()". Puis si les variables sont modifiées par l'exécution d'une commande, elles sont réaffichées dans leur zone de saisie par la fonction "WRITE()". La macro µ va donc également comléter le code ces quatres fonctions :

function INIT(){
$Nom = document.getElementById("Nom") ... READ()
PAGE()
RUN()
WRITE() }

Les instructions d'initialisation dépendent du type de macro utilisé µ, µa, µi, µf et sont regroupées dans la fonction "READ()" :

function READ(){  // lit les variables qui sont dans les input et les aera
 Nom = $Nom.value
 Nom = $Nom.value  Nom = ParseInt($Nom.value)  Nom = ParseFloat($Nom.value) ... }

Puis certaines variables sont des caractéristiques d'élément de la page web. Par exemple, si NX et NY sont les variables spécifiant la taille du canvas, on modifie la taille du canvas à l'aide des instructions suivantes que l'on place dans la fonction "PAGE()" qui est exécuté après l'exécution de la fonction "PAGE()"

function PAGE(){ // modifie la page web selon les nouvelles valeurs des variables
 $W.setAttribute("width",NX)
 $W.setAttribute("height",NY) 
 ...
}

Puis l'exécution d'une commande peut modifier les variables et nécessite alors de les réafficher dans leur zone de saisie dans la page web. Cela se fait en lancant la fonction "WRITE()" :

function WRITE(){ // écrit les variables dans leur zone de saisie (input et aera)
 $Nom.value=Nom
 $Nom.value=Nom.toFixed(3) ... }

L'ajout de variables supplémentaires internes se fait par le jeu d'instruction javascript ordinnaire.

3) Gestionnaire d'évènements

Le javascript est un langage de programmation évènementielle. Le premier évènement que nous utilisons se produit lorsque la page web est chargé. On ajoute à l'élément "window" le gestionnaire d'évènement "load" que l'on associe à la fonction "INIT()".

window.addEventListener("load", INIT, false)

Délors, dés que la page web sera chargé dans le navigateur la fonction "INIT()" se lancera. On choisi qu'un seul type d'élément de commande, sous forme de boutons regroupé dans un tableau à une seule colonne. La macro ß(Nom,"Libelle", "Description"){...} va crée le bouton B_NomLibelle sera son libellé, et où Description est optionnel et sera sa description, et où {...} est le programme associée, et va coder en html :

<tr><td><button title="Description" onclick="B_Nom()">Libelle | <span class="var">Nom<\span></button></td></tr>

et va coder en js :

function B_Nom(){
 ...
}

Le second type d'évènement que nous mettons en oeuvre est le "onclique" dans un canvas. On crée un canvas W dont l'élément est $W. Lorsque le constructeur d'application rencontre la macro £ :

£(Nom, "mx" , "my")

Nom est une chaine de caractères contenant le nom de la variable, où mx, my sont les noms de variables déjà définie donnant la taille du canevas il va générer dans le fichier html le code suivant à 4 endroits:

let Nom 	// Canvas
let $Nom // Elément du DOM associé
<canvas id="W"></canvas>


function INIT(){
 ...
$Nom = document.getElementById("Nom") Nom = $Nom.getContext("2d") $Nom.onclick=function(e){ READ()
PAGE() let r = $Nom.getBoundingClientRect() clique(e.pageX - r.x - window.scrollX, e.pageY - r.y - window.scrollY) READ()
PAGE()
RUN()
WRITE() } function PAGE(){
$W.setAttribute("width",NX)
$W.setAttribute("height",NY) ...
}

Le gestionnaire d'évènements actualise et lance la fonction clique en passant en argument les coordonnées de la souris relative au canvas en tenant compte de l'état des ascenseurs du navigateur.

II

L'approche génésique

 

1) Introduction

L'approche génésique développe l'analogie entre le développement d'une application et le développement d'un être vivant, mais à la différence de la vie, à chaque étape le développeur va argumenter son choix d'évolution. Le developpement de l'application se fait en même temps que la maîtrise ou le dévelopement d'un environnement, le tout à partir de zéro. Appliqué à notre étude, "L'étude des points convergeants d'une fonction récurcive", l'approche génésique va non seulement nous entrainer dans le développement d'un environnement, d'une bibliothèque, d'un framework... mais va également nous orienter dans l'analyse mathématique du problème. Car l'application ouvrant de nouvelles possibilités de calcul soulèvera des questions aux quelles on n'avait pas pensé avant.

2) Martrice des données

L'application calcul pour chaque point `(x,y)` d'une portion du plan le nombre d'itérations avant que la valeur `x^2+y^2` ne dépasse un seuil `R` à partir du quel on considère que la fonction récurcive diverge. On mémorise les `NX*NY` points pixels du plan de coordonnée [0,NX]×[0,NY]. Pour chacun d'eux `(X,Y)` ont fait correspondre un point réel de coordonnée `(x,y)` obtenu par un déplacement et un zoomage décrit par l'application suivante :

`(X,Y)|->(aX+u,aY+v)`

Il y a donc trois paramètres qui précise l'emplacement et le zoom :

Il convient de déterminer les paramètres initiaux ainsi que leur transformation lors d'opération de zoomage et de dézoomage.

1) Détermination du zoom initial

Le zoom est une transformation linéaire du point `(X,Y)` qui désigne le pixel entier, en le point réel `(a"∗"X"+"u,a"∗"Y"+"v)`.

`(X,Y)|->(a"∗"X"+"u,a"∗"Y"+"v)`

On choisit un zoom initial qui donne une distance réel de 4 pour la largeur du canevas `tt"NX"`, donc :

`a=4/ tt"NX"`

Puis le point milieu `(tt"NX"/2,tt"NY"/2)` doit être transformé en le point réel `(0,0)`, c'est à dire :

`a"∗"tt"NX"/2+u = 0`      et      `a"∗"tt"NY"/2+v = 0`

Donc :

`u=-a"∗"tt"NX"/2`      et      `v=-a"∗"tt"NY"/2`

2) Détermination du zoomage

Lorsque on clique sur le point pixel de coordonnée `(X_1,Y_1)`, on augmente le zoom en divisant a par `Z=2`. La nouvelle transformation linéaire est :

`(X,Y) |-> (a/Z"∗"X+U, a/Z"∗"Y+V)`

`U` et `V` sont des paramètres inconnus. Puis le point milieu `(tt"NX"/2,tt"NY"/2)` doit être transformé en le point réel `(a"∗"X_1+u,a"∗"Y_1+v)`, c'est à dire :

`a/Z"∗"tt"NX"/2+U=a"∗"X_1+u`      et      `a/Z"∗"tt"NY"/2+V=a"∗"Y_1+v`

Donc :

`U=a"∗"X_1+u- a/Z"∗"tt"NX"/2`      et      `V = a"∗"Y_1 + v - a/Z"∗"tt"NY"/2`

3) Détermination du dézoomage

Réciproquement, on établit une transformation inverse appelée dézoomage. Pour des questions de confort, l'opération de dézoomage doit être centré. C'est à dire que le point central du canvas doit rester fixe lors du dézoomage. La transformation de dézoomage est une transformation linéaire du point `(X,Y)` qui désigne le pixel entier, en le point réel `(a"∗"Z"∗"X"+"U,a"∗"Z"∗"Y"+"V)``U` et `V` sont des paramètres inconnus.

`a"∗"tt"NX"/2+u = a"∗"Z"∗"tt"NX"/2"+"U`      et      `a*tt"NY"/2+v = a"∗"Z"∗"tt"NY"/2"+"V`

Donc :

`U = a"∗"tt"NX"/2+u - a"∗"Z"∗"tt"NX"/2`      et      `V=a"∗"tt"NY"/2+v-a"∗"Z"∗"tt"NY"/2`

3) La vue

Le calcul peut être très long, et une fois le calcul fait, il peut être affiché de façon trés différentes selon plusieurs autres paramètres. On doit donc concevoir une deuxième partie où seul les paramètres de vue change.

La donnée calculée au point pixel `(X,Y)` est mémorisée dans une matrice `M[X][Y]` sous forme de couple `[.,.]`. La première composante est le nombre d'itérations avant divergence (borné à Nmax), la seconde composante est la valeur résultat de la dernière itération.

7) Le web worker

Chaque worker s'exécute dans un nouveau thread et n'est pas limité dans le temps. Les ordinateurs à plusieurs coeurs peuvent exécuter en parallèle plusieurs workers, qui s'exécuteront réellement simultanément. On lance un worker pour calculer chaque ligne du caneva. Si le nombre de workers devient trop grand, il faut alors attendre la fin de travail d'un worker avant dans lancer un autre.

---- 8 février 2024 ----