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.