Accès direct au résultat
Le problème
Nous voulons réaliser une système illustrant les transformations applicables à tout élément composant une image SVG. Ces transformations sont :
- Les translations pour déplacer les éléments,
- les rotations pour faire pivoter les éléments,
- les cisaillements pour déformer les éléments par exemple en transformant un carré en losange,
- les grossissements pour grossir, diminuer mais aussi faire une effet miroir (grossissement négatifs),
- et enfin les transformations matricielles permettant en une seule opération n'importe quelle combinaison des transformations précédentes.
Étant donné que les transformations matricielles permettent à elles seules toutes les autres, nous allons réaliser un système n'utilisant que ces transformations et à partir duquel on indiquera par une combinaison de rotation, translation, grossissement et cisaillement l'opération à réaliser.
Pour illustrer chacune de ces transformations il suffira donc :
- de calculer la matrice en fonction des transformations désirées,
- d'appliquer cette matrice à l'élément concerné dans l'arbre XML que constitue l'image SVG.
Le principe est simple mais pour utiliser une méthode interactive 100% SVG il faudra créer des éléments de saisie semblables à des curseurs et modifiables par exemple à l'aide de la souris, ce qui est tout-à-fait réalisable par un script javascript.
La solution
Pour commencer regardons l'image SVG à réaliser.
L'image au format SVG
L'image qui subira les transformations sera très simple dans la mesure ou toute la partie contrôle est gérée par le javascript. Elle devra intégrer :
- les entêtes XML et SVG,
- un lien au fichier contenant les scripts javascript (attention à ne pas oublier l'espace de nommage http://www.w3.org/1999/xlink permettant d'intégrer des liens dans un document XML,
- un fichier de style CSS histoire d'habiller un peu notre image,
- une figure géométrique à transformer fournie en deux exemplaires :
- le premier restera fixe,
- le second sera transformé par la matrice via le javascript,
- un élément SVG de type <g> (groupe d'éléments) destiné à recevoir les contrôles générés par javascript,
- un autre élément SVG de type <g> destiné à afficher la matrice de transformation calculée par javascript.
il faudra donc fournir les fichiers suivants :
- Le document SVG dont le code est donné ci-dessous :
<?xml-stylesheet type="text/css" href="svg_style.css" ?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" version="1.0" viewBox="0 0 700 400" id="svg2"> <!-- chargement des comportements entièrement déportés dans les fichiers cursors.es et matrix.es --> <script xlink:href="cursors.es" type="text/ecmascript"/> <script xlink:href="matrix.es" type="text/ecmascript"/> <defs> <!-- les marqueurs pour le repère Oxy --> <marker id="cercle" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" viewBox="-2 -2 4 4" orient="auto"> <circle r="2" cx="0" cy="0" style="stroke-width:0.2px;stroke:#666"/> </marker> <marker id="fleche" markerWidth="14" markerHeight="14" orient="auto" refY="5" refX="9"> <path d="M0,1 L10,5 0,9"/> </marker> </defs> <!-- les 2 éléments à representer l'un avant et l'autre après transformation --> <g transform="translate(200,150)"> <g id="original"> <rect id="r_original" x="0" y="0" width="100" height="100" style="fill:#888"/> <line x1="0" y1="0" x2="0" y2="120" class="axe" marker-start="url(#cercle)" marker-end="url(#fleche)"/> <text x="140" y="-8" class="axe">→</text> <text x="140" y="0" class="axe">ox</text> <line x1="0" y1="0" x2="120" y2="0" class="axe" marker-start="url(#cercle)" marker-end="url(#fleche)"/> <text x="0" y="130" class="axe">→</text> <text x="0" y="140" class="axe">oy</text> <text x="50" y="50" class="face">Original</text> </g> <g id="transforme"> <rect id="r_transforme" x="0" y="0" width="100" height="100" style="fill:#FCC"/> <line x1="0" y1="0" x2="0" y2="120" class="axe" marker-start="url(#cercle)" marker-end="url(#fleche)"/> <text x="140" y="-8" class="axe">→</text> <text x="140" y="0" class="axe">ox'</text> <line x1="0" y1="0" x2="120" y2="0" class="axe" marker-start="url(#cercle)" marker-end="url(#fleche)"/> <text x="0" y="130" class="axe">→</text> <text x="0" y="140" class="axe">oy'</text> <text x="50" y="50" class="face">transformé</text> </g> </g> <!-- les curseurs des transformations à appliquer --> <g id="matrix" transform="translate(10, 20)"/> <g id="cursors" transform="translate(0, 300)"/> </svg>
Ce document intègre deux fichiers javascript, ceci permet de séparer ce qui est spécifique à cette application de ce qui peut être utilisé ailleurs ; c'est pourquoi nous avons ici :
- le fichier cursors.es qui contient la partie IHM pour la gestion des curseurs,
- le fichier matrix.es ne contenant que le code spécifique à cette application.
- la feuille de style utilisée (indiquée par la première ligne <?xml-stylesheet type="text/css" href="svg_style.css" ?>) et dont le code est ici :
text { -o-user-select:none; -ms-user-select:none; -moz-user-select:none; -webkit-user-select:none; user-select:none; } text.face { font-size:15px; text-anchor:middle; alignment-baseline:middle; text-decoration:underline; } text.axe { font-size:20px; text-anchor:middle; alignment-baseline:middle; } line.vect { stroke-linecap:butt; stroke-width:4; } line.axe { stroke:#000; stroke-width:2; stroke-dasharray:10 2 1 2; } text.title { text-anchor:middle; } path { fill:#AAA; fill-opacity:0.4; stroke-width:0.5px; stroke:#000; stroke-dasharray:0; stroke-width:2; stroke-linejoin:miter; stroke-miterlimit:4.0; } #matrix .delim { font-size:150px; fill:#666; text-anchor:middle; alignment-baseline:middle; }
Maintenant que l'image est construite regardons l'IHM à réaliser.
Les éléments d'interface
Nous voulons réaliser les opérations suivantes :
- translation horizontale de -300 pixels à 300 pixels,
- translation verticale de -300 pixels à 300 pixels,
- grossissement horizontal de -3 à 3,
- grossissement vertical de -3 à 3,
- cisaillement horizontal de -89° à 89° (on évitera ainsi la division par 0 présente à 90°),
- cisaillement vertical de -89° à 89°,
- rotation de 0 à 360°.
Il nous faudra donc une interface pourvue de 7 curseurs couvrant les plages des valeurs correspondantes.
Pour cela nous procéderons en 5 étapes :
la déclaration de l'interface
Afin de simplifier la maintenance du script nous allons créer dynamiquement les éléments de contrôle qui seront donc définis à partir d'un tableau associatif contenant :
- min qui sera la valeur minimale de la plage,
- max qui sera la valeur maximale de la plage,
- def qui sera la valeur par défaut,
- style qui sera le style CSS à appliquer,
- txt qui sera le label à placer au dessus du curseur.
Nous placerons ensuite chaque élément dans le tableau associatif controls en utilisant comme clef le nom du contrôle.
Ainsi le code du tableau décrivant nos 7 curseurs (propre à cette application et donc placé dans matrix.es) sera :
controls = {tx: {min: -300, max:300, def:300, cursorStyle:'fill:#00F', txt: 'tr X'}, ty: {min: -300, max:300, def:-100, cursorStyle:'fill:#00F', txt: 'tr Y'}, rot: {min: 0, max: 360, def: 45, cursorStyle:'fill:#0FF', txt: 'rot'}, zx: {min: -3, max:3, def:1.5, cursorStyle:'fill:#F0F', txt:'Zoom X'}, zy: {min: -3, max:3, def:1.5, cursorStyle:'fill:#F0F', txt:'Zoom Y'}, cx: {min: -89, max: 89, def:0, cursorStyle:'fill:#FF0', txt: 'Skew X'}, cy: {min: -89, max: 89, def:0, cursorStyle:'fill:#FF0', txt: 'Skew Y'}};
Par ailleurs nous devrons utiliser plusieurs variables globales nécessaires au fonctionnement de l'IHM (et donc placées dans cursors.es) dont le code de déclaration est le suivant :
var svgNS = "http://www.w3.org/2000/svg"; var grab = {}; // destiné à mémoriser l'élément sélectionné et ses coordonnées var cursors = []; // tableau des éléments curseurs var ratio = 1; // facteur de corresponcance entre pixels de l'image et déplacement du curseur
la création des éléments
Nous allons maintenant créer des éléments de contrôle (toujours dans cursors.es) constitués de 2 rectangles :
- le curseur qui se déplacera à l'aide de la souris,
- la course du curseur qui sera fixe,
- un rectangle commun à tous les éléments définira la zone de contrôle.
La fonction de génération des curseurs initCursors()
Cette fonction assure le dessin des curseurs dans l'élément SVG identifié par id="curseurs" et dont le code est :
function initCursors() { var cursor_style = 'fill:#F00'; // style par défaut du curseur var cursor_bgstyle = 'fill:#444'; // style par défaut de la zone de course du curseur var cursor_shift = 100; // écart horizontal entre 2 curseurs consécutifs var cursor_height = 10; // hauteur du curseur var cursor_width = 10; // largeur du curseur var cursor_slide_height = 80; // longueur de la course du curseur var cursor_slide_width = 5; // largeur de la course du curseur var cursor_Y_pos = 15; // position verticale de la course du curseur var cursor_textX_pos = 10; // position horizontale du label du curseur var cursor_textY_pos = 12; // position verticale du label du curseur var cursor_valX_pos = 60; // position horizontale de la valeur numérique liée au curseur var cursor_valY_pos = 50; // position verticale de la valeur numérique liée au curseur var cursors_bgstyle = 'fill:#CCC;fill-opacity:0.5'; var ndcrs = document.getElementById('cursors'); var fondCurseurs = document.createElementNS(svgNS, 'rect'); fondCurseurs.setAttribute('x', 0); fondCurseurs.setAttribute('y', 0); fondCurseurs.setAttribute('height', cursor_slide_height+20); fondCurseurs.setAttribute('style', cursors_bgstyle); ndcrs.appendChild(fondCurseurs); // création des curseurs var i = 0; // utile pour calculer la position du curseur for(var crs in controls) { var elmnt = {} cursors[crs] = elmnt; // création du champ texte contenant la valeur elmnt.text = document.createElementNS(svgNS, 'text'); elmnt.text.textContent = controls[crs].def; elmnt.text.setAttribute('x', cursor_valX_pos + i*cursor_shift - cursor_width/2); elmnt.text.setAttribute('y', cursor_valY_pos); // création du rectangle représentant le curseur elmnt.cursor = document.createElementNS(svgNS, 'rect'); elmnt.cursor.setAttribute('x', (i+0.5)*cursor_shift - cursor_width/2); elmnt.cursor.setAttribute('width', cursor_width); elmnt.cursor.setAttribute('height', cursor_height); elmnt.cursor.setAttribute('style', cursor_style); elmnt.cursor.setAttribute('id', 'curseur_'+crs); // création du rectangle représentant la course du curseur elmnt.fond = document.createElementNS(svgNS, 'rect'); elmnt.fond.setAttribute('x', (i+0.5)*cursor_shift - cursor_slide_width/2); elmnt.fond.setAttribute('y', cursor_Y_pos); elmnt.fond.setAttribute('width', cursor_slide_width); elmnt.fond.setAttribute('height', cursor_slide_height); elmnt.fond.setAttribute('style', cursor_bgstyle); // mémorisation des amplitude et offset du curseur pour calcul de la valeur à attribuer elmnt.ampY = parseFloat(elmnt.fond.getAttribute('height')) - parseFloat(elmnt.cursor.getAttribute('height')); elmnt.minY = parseFloat(elmnt.fond.getAttribute('y')); // positionnement à la valeur par défaut posFromValue(crs); ndcrs.appendChild(elmnt.fond); ndcrs.appendChild(elmnt.cursor); ndcrs.appendChild(elmnt.text); // affichage du label si celui-ci existe (champ txt de controls) if (controls[crs].txt) { var textd = document.createElementNS(svgNS, 'text'); textd.textContent = controls[crs].txt; textd.setAttribute('x', cursor_textX_pos + i*cursor_shift - cursor_width/2); textd.setAttribute('y', cursor_textY_pos); ndcrs.appendChild(textd); } // application du style si celui-ci existe (champ style de controls) if (controls[crs].cursorStyle) elmnt.cursor.setAttribute('style', controls[crs].cursorStyle); i++; } // mise en place du fond des curseurs à la dimension attendue fondCurseurs.setAttribute('width', i*cursor_shift); transform(); }
l'interaction
Il faut maintenant gérer les événements en provenance de la souris (toujours dans cursors.es). Ces événements sont :
- le
click
qui repère quel curseur est concerné et le mémorise, - le
drag
qui déplace le curseur visé par leclic
précédent, - le
release
qui interrompt toute action sur le curseur visé par leclic
précédent.
Les fonctions ci-dessous assurent ces opérations :
La fonction grip(evt)
Cette fonction est en charge de la mémorisation :
- de l'élément sollicités lors du clic,
- des coordonnées le l'élément lors du clic.
function grip(evt) { for(var ct in controls) { if (evt.target.id == 'curseur_'+ct) { grab.elmt = ct; } } if (grab.elmt) { grab.cy = parseFloat(cursors[grab.elmt].cursor.getAttribute("y")); grab.Y = evt.clientY; } }
La fonction drag(evt)
Cette fonction assure le déplacement du curseur et la régénération du dessin :
- elle déplace verticalement l'élément du tableau des curseurs cursor visé par grab.elmt,
- elle lance ensuite la procédure de dessin transform().
function drag(evt){ if (!grab.elmt) return; var Y = evt.clientY; var y0 = cursors[grab.elmt].minY; var s = cursors[grab.elmt].ampY; var y = Math.min(Math.max(grab.cy+(Y-grab.Y)*ratio, y0), y0+s); cursors[grab.elmt].cursor.setAttribute('y',y); cursors[grab.elmt].text.textContent = valueFromPos(grab.elmt); transform(); }
La fonction released()
Cette fonction abandonne la sélection et donc la mémorisation du curseur.
function released(){ grab.elmt = undefined; }
Les fonctions d'aide au maintien des valeurs
Afin de rationaliser la correspondance valeurs numériques / position des curseurs nous utilisons 2 fonctions dont le rôle est :
- de récupérer la valeur à partir de la position du curseur, c'est la fonction valueFromPos(elmt) :
function valueFromPos(elmt) { var s = cursors[elmt].ampY; var y0 = cursors[elmt].minY; var y = cursors[elmt].cursor.getAttribute('y'); var min = controls[elmt].min; var max = controls[elmt].max; var value = (min + (max-min)*(1-(y-y0)/s)).toFixed(1); return value; }
- de mettre le curseur à sa position à partir de sa valeur, c'est la fonction posFromValue(elmt) dont le code est :
function valueFromPos(elmt) { var s = cursors[elmt].ampY; var y0 = cursors[elmt].minY; var min = controls[elmt].min; var max = controls[elmt].max; var def = parseFloat(cursors[elmt].text.textContent); var y = y0 + s * ((max - def)/(max-min)); cursors[elmt].cursor.setAttribute('y', y); }
- le
l'application de la transformation.
D'après le code des fonctions drag() et initCursors() on voit que la fonction transform() est appelée à chaque action sur un curseur. c'est donc elle qui sera spécifique à notre application (et donc dans matrix.es) et qui devra :
- calculer le matrice de transformation,
- appliquer celle-ci à l'image SVG.
Rappel : l'opération de transformation sera donc appliquée par la méthode transform() qui utilise les valeurs stockées dans des variables remises à jour par la partie contrôle pilotée via les curseurs. Le nom de ces variable est cursors[ctrl].text (où ctrl désigne le nom du contrôle).
Nous allons donc créer la fonction calculant la matrice à partir ce ces valeurs.Pour cela nous réaliserons plusieurs fonctions javascript comme décrit ici :
la fonction de transformation de matrice transformMatrix(matrix)
Le rôle de cette fonction sera de convertir une matrice en coordonnées généralisées, c'est à dire apte à produire simultanément des translations et des déformations, et donc représentée sous la forme d'un tableau de 3x3 vers une chaîne de caractères correspondant aux attentes du SVG.
Il faudra donc fournir une chaîne de la forme matrix(v1,v2,v3,v4,v5,v6).
Cette fonction est donnée par le code suivant :
function TransformMatrix(matrix) { return 'matrix('+matrix[0][0].toFixed(2)+','+matrix[1][0].toFixed(2)+',' +matrix[0][1].toFixed(2)+','+matrix[1][1].toFixed(2)+',' +matrix[0][2].toFixed(2)+','+matrix[1][2].toFixed(2)+')'; }
la fonction produit de matrices 3x3 prodMatrix(m1, m2)
Son rôle sera de produire une matrice 3x3 résultant du produit de 2 matrices 3x3.
Cette fonction est donnée par le code suivant :
function prodMatrix(m1, m2) { var res = new Array(); for (var j=0; j<m1.length; j++) { res[j] = new Array(); for (var k=0; k<m2[j].length; k++) { res[j][k] = 0; for (var i=0; i<m2[j].length; i++) { res[j][k] += m1[j][i] * m2[i][k]; } } } return res; }
la fonction de génération de la matrice à partir des curseurs getMatrix()
Son rôle sera de produire la matrice à appliquer à l'image SVG à partir des curseurs de l'interface. Elle aura donc recours aux procédures précédentes pour combiner les différentes opérations réalisées : translation + rotation + grossissement + cisaillement.
Cette fonction est donnée par le code suivant :
function getMatrix() { var tx = parseFloat(cursors['tx'].text.textContent); var ty = parseFloat(cursors['ty'].text.textContent); var zx = parseFloat(cursors['zx'].text.textContent); var zy = parseFloat(cursors['zy'].text.textContent); var cx = parseFloat(cursors['cx'].text.textContent); var cy = parseFloat(cursors['cy'].text.textContent); var teta_rot = parseFloat(cursors['rot'].text.textContent) * Math.PI / 180; var teta_cx = parseFloat(cursors['cx'].text.textContent) * Math.PI / 180; var teta_cy = parseFloat(cursors['cy'].text.textContent) * Math.PI / 180; var cos_r = Math.cos(teta_rot); var sin_r = Math.sin(teta_rot); var tan_cx = Math.tan(teta_cx); var tan_cy = Math.tan(teta_cy); var mat_rt = [[cos_r, -sin_r, tx], [sin_r, cos_r, ty], [0, 0, 1]]; var mat_cxy = [[1, tan_cx, 0], [tan_cy, 1, 0], [0, 0, 1]]; var mat_zm = [[zx, 0, 0], [0, zy, 0], [0, 0, 1]]; var matrix = prodMatrix(prodMatrix(mat_rt, mat_zm), mat_cxy); return matrix; }
la fonction d'affichage d'une matrice 3x3 DisplayMatrix(matrix)
Afin d'illustrer l'impact des transformations sur les coefficients de la matrice nous allons créer une fonction destiné à afficher son contenu dans l'image SVG. Cette fonction utilisera l'élément <g id="matrix"> pour faire le rendu.
Elle est donnée par le code suivant :
function DisplayMatrix(matrix) { // la matrice ne sera affichée que si l'élément 'matrix' existe var mid = document.getElementById('matrix'); if (mid) { for (var j in matrix) { for (var i in matrix[j]) { var id = 'matrix_xy_'+i+'_'+j; var nd = document.getElementById(id); if (!nd) { nd = document.createElementNS(svgNS, 'text'); nd.setAttribute('x', i * 50); nd.setAttribute('y', j * 50); nd.setAttribute('id', id); mid.appendChild(nd); } nd.textContent = matrix[j][i].toFixed(2); } } } }
fonction d'application de la transformation transform()
Pour finir il ne reste plus qu'a appliquer les changements via la fonction transform() appelée par l'interface de contrôle.
celle-ci assurera les opérations suivantes :
- elle appliquera la matrice retournée par getMatrix() à l'élément dont l'identité est transforme,
- elle affichera les valeurs de la matrice appliquée par DisplayMatrix().
Elle est donnée par le code suivant :
function transform() { var gid = document.getElementById('transforme'); var matrx = getMatrix(); //application du de la transformation SVG gid.setAttribute('transform',TransformMatrix(matrx)); //affichage de la matrice 3x3 DisplayMatrix(matrx); }
mise en place des
bindings
Toutes les fonctions sont maintenant écrites mais il faut les attacher à des événements javascript, ce que nous allons faire maintenant.
Pour des raisons de découplage entre l'image SVG et le script javascript nous déclencherons la production des éléments en fin de chargement de l'image SVG et ce à l'aide de la propriété window.onload (à placer dans cursors.es).function DisplayMatrix(matrix) { window.onload = function() { initRatio(); initCursors(); window.onresize = initRatio; window.onmouseup = released; window.onmousemove = drag; window.onmousedown = grip; }
Il faudra également écrire la fonction initRatio() destinée à calculer la correspondance entre un pixel image et un pixel déplacement curseur souris. Cela aura le bon goût de conserver le curseur de la souris sur le curseur SVG piloté par l'interface et ce même lors du déplacement.
Cette fonction n'est pas indispensable et si on ne veut pas l'utiliser il suffit éliminer le
binding
nommé window.onresize de la procédure d'initialisation.Cette fonction initRatio() peut s'écrire ainsi :
function initRatio() { var relmt = document.getElementsByTagName('svg')[0]; if (relmt.clientWidth && relmt.clientHeight) { // n'est utilisé que par les navigateurs disposant des propriétés clientWidth et clientHeight ratio = Math.max(relmt.viewBox.animVal.width/relmt.clientWidth, relmt.viewBox.animVal.height/relmt.clientHeight); } }
Et c'est fini, il n'y a plus qu'a charger l'image SVG dans un navigateur et jouer avec, mais si vous n'avez pas le courage de copier/coller tous les fragments de code de cet exercice vous pouvez directement voir et agir sur le résultat ci-dessous :