David Madore's WebLog: Le JavaScript, le DOM et les petites crottes de ragondins

Index of all entries / Index de toutes les entréesXML (RSS 1.0) • Recent comments / Commentaires récents

Entry #2513 [older|newer] / Entrée #2513 [précédente|suivante]:

(jeudi)

Le JavaScript, le DOM et les petites crottes de ragondins

J'ai écrit dans l'entrée précédente un petit bout de code en JavaScript permettant de générer de jolies images de pavages quasipériodiques. Forcément, c'est le genre de choses qui donne envie d'ajouter une fonctionnalité, puis une autre, puis encore une autre : choix du jeu de couleurs, choix de l'échelle, ce genre de choses. Au fur et à mesure, le code tend à se compliquer et à devenir un peu illisible (surtout que je suis parti de quelque chose d'un peu ancien dont je ne me rappelais plus exactement comment il fonctionnait — raison pour laquelle mes explications dans l'entrée en question étaient un peu confuses).

Là, je viens d'ajouter une fonctionnalité permettant de déplacer la région qu'on voit (il faut cliquer quelque part, déplacer la souris en gardant le bouton appuyé, et relâcher le clic). C'était assez naturel de souhaiter ça : après tout, l'image représente un bout d'un pavage infini du plan, on a envie de pouvoir explorer ce qui se passe quand on s'éloigne de l'origine. Double difficulté, la première étant due au fait que je n'avais pas du tout prévu ça dans mon code initial — là je ne peux m'en prendre qu'à moi-même —, la seconde étant qu'en JavaScript+DOM, tout est toujours plus compliqué que prévu.

Notamment, à chaque fois que le navigateur fournit une information, par exemple sur un événement (clic de souris, ce genre de choses), il vous fournit quantité de choses qui vont avec, mais on a l'impression que ces choses ont été savamment choisies pour donner l'impression d'être utiles mais être, au final, vraiment pénibles à utiliser.

Un exemple qui me semble frappant : je crée mon image dans un <canvas> HTML (c'est-à-dire une région prévue, justement, pour qu'on puisse dessiner dessus en JavaScript). Le <canvas> en question a une certaine taille, dans mon cas j'ai choisi 1024×768 pixels. (Enfin, pixel, il faut le dire vite, parce que la notion de pixel sur le web est un vrai chaos et c'est devenu une unité de mesure comme une autre. Mais disons que je peux faire comme si j'avais une image de 1024×768 pixels sur laquelle je peux travailler.) Sauf que comme l'image peut vite devenir trop grande sur un mobile, j'ai utilisé une incantation CSS style="max-width: 100%; height: auto" sur mon <canvas> pour demander au navigateur que, si l'image est trop grande pour tenir dans l'élément enveloppant, il la réduise à la taille appropriée (et ce, de façon proportionnée) ; je ne suis pas sûr de comprendre les subtilités, d'ailleurs, mais bon, ça a l'air de marcher. Maintenant, quand le navigateur transmet une information concernant un clic de souris (disons), il donne plusieurs copies des coordonnées du clic : par rapport à l'écran (screenX et screenY), par rapport à la page complète (pageX et pageY), par rapport au contenu effectivement affiché de la page (clientX et clientY), par rapport à on ne sait pas bien quel bord de l'élément lui-même (offsetX et offsetY), et par rapport à un truc que personne ne semble comprendre (layerX et layerY). On s'y perd complètement ! Mais ce qui est vraiment hallucinant, c'est qu'aucune de ces nombreuses coordonnées n'est celles qu'on veut vraiment, c'est-à-dire celles par rapport au <canvas> dans son système de coordonnées par défaut ! (Autrement dit, quelque chose qui renverrait un nombre entre 0 et la largeur/hauteur en pixels telle que précisée dans l'attribut width ou height de l'élément. Ou éventuellement, dans son système de coordonnées transformé par les opérations de translations, rotations et d'échelles qu'on a pu appliquer ultérieurement.) Et, pire, il n'existe aucun mécanisme simple pour faire la conversion : en m'inspirant d'informations glanées çà et là sur le Web, je me suis retrouvé à écrire l'horreur suivante :

function undoCSSFuckery(e) {
    // Compute event x and y relative to canvas which may be scaled by
    // CSS (***sigh!***).
    "use strict";
    var x,y;
    var rect = e.target.getBoundingClientRect();
    var clX = typeof(e.changedTouches)=="undefined" ? e.clientX : e.changedTouches[0].clientX;
    var clY = typeof(e.changedTouches)=="undefined" ? e.clientY : e.changedTouches[0].clientY;
    x = clX - rect.left;
    y = clY - rect.top;
    var width = rect.right - rect.left;
    var height = rect.bottom - rect.top;
    if ( e.target.width != width || e.target.height != height ) {
        x *= (e.target.width/width);
        y *= (e.target.height/height);
    }
    return [x,y];
}

— qui n'est certainement pas robuste parce que, par exemple, si le <canvas> a des marges ça ne va probablement pas marcher. Ah oui, il y a moyen d'interroger CSS pour connaître toutes sortes de choses, mais il faut prononcer un nombre impressionnant d'incantations propitiatoires pour le faire, et le résultat est souvent encore à déchiffrer derrière : par exemple, il va souvent renvoyer des informations en pixels sous forme de chaînes du type "1024px", et on ne sait jamais si ce suffixe px est garanti être là, ou comment on doit se débrouiller si une autre unité devait être retournée (le navigateur n'expose évidemment pas au JavaScript tous les mécanismes de conversions d'unités qu'il a forcément en lui-même !).

On a vraiment l'impression que tout est fait pour mettre des bâtons dans les roues du programmeur : lui donner douze fois la même information de douze façons subtilement différentes pour l'embrouiller, et pour qu'il ne puisse pas savoir ce qui va marcher sur quel navigateur, mais surtout ne pas lui donner l'information qu'il veut vraiment, et le forcer à la calculer en utilisant, du coup, encore d'autres informations dont on ne sait pas ce qu'elles valent.

Le support du mouvement de la souris est du même acabit : je réagis aux événements "mousedown" et "mouseup" pour la souris, et aussi "touchstart" et "touchend" pour les interfaces tactiles, mais j'ai cru comprendre que les interfaces tactiles généraient typiquement ausis du "mousedown" et "mouseup", et qu'en utilisant e.preventDefault() j'allais éviter ça — mais je ne sais pas si j'ai fait ce que j'aurais dû. Et il n'y a évidemment aucun moyen fiable de s'assurer qu'on est en train de bien associer le bon événement "mouseup" au bon "mousedown" au cas où des événements se seraient perdus en route (il n'y a pas, par exemple, moyen de sauvegarder une information au moment du "mouseup" qui serait garantie être retournée par le "mousedown" correspondant). Quelle horreur !

Dans ces conditions, il n'est pas surprenant que fleurissent toutes sortes de bibliothèques et frameworks JavaScript (du style jQuery), qui facilitent certaines tâches mais augmentent la lourdeur du code et son temps de chargement, et ajoutent leur propre complexité à la moussaka en surchargeant des choses qui existent déjà et en rendant plus complexe la recherche de solutions en ligne (notamment pour distinguer les solutions en pur JavaScript et celles en jQuery ou équivalent). D'ailleurs, que je sache, jQuery ne fournit pas l'équivalent de ma fonction undoCSSFuckery() ci-dessus, ce qui est quand même impressionnant pour une bibliothèque censée tout faire même le café.

Bref, mon code pour déplacer dans les images de l'entrée précédente marche peut-être. Parfois. Certains jours. Sur certains navigateurs. Si vous voyez comment faire plus propre, n'hésitez-pas à me le dire.

↑Entry #2513 [older|newer] / ↑Entrée #2513 [précédente|suivante]

Recent entries / Entrées récentesIndex of all entries / Index de toutes les entrées