Animation par projection 3D SVG

entre Vous et Nous

Accès direct au résultat

Le problème

Nous voulons simuler l'animation d'un objet dans l'espace à l'aide d'une projection plane réalisée par des images SVG.

L'idée de cet exercice est d'imaginer un objet collé à une tige et de représenter cet objet projeté sur le plan tout en faisant tourner la tige sur elle-même. La tige peut être articulée de façon quelconque par rapport à l'objet.

Les interactions devront donc permettre les 3 degrés de liberté offerts par les rotations sur cette tige qui deviendra l'axe de rotation de l'objet.

Une représentation de cet axe peut être de supposer qu'il corresponde au rayon d'une sphère et donc qu'il est contrôlé par :

Pour cela nous aurons recours aux transformations affines à l'aide de matrices 4x4 que nous projetterons sur le plan SVG ce qui exigera :

Toute les opérations matricielles sont réalisables par un script javascript que nous allons élaborer ici.

La solution

ici aussi il faudra créer une image en SVG intégrant un script javascript

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 :

Afin de récupérer un maximum de code utilisable pour plusieurs applications précédents l'image charge trois scripts :

Il faudra donc fournir le document SVG dont le code est peut être celui 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 600 400"
   id="svg2">
  <!-- chargement des comportements entièrement déportés dans le fichier rotate.es -->
  <script xlink:href="cursors.es" type="text/ecmascript"/>
  <script xlink:href="utils_3d.es" type="text/ecmascript"/>
  <script xlink:href="rotate.es" type="text/ecmascript"/>
  <defs>
    <!-- les marqueurs pour le repère Oxyz -->
    <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="4"
            markerHeight="4"
            orient="auto"
            refY="2"
            refX="3">
    <path d="M0,0 L4,2 0,4"/>
    </marker>
  </defs>
  <!-- les 3 faces du cube à faire tourner -->
  <g class="orth" transform="translate(200,150)">
    <g>
      <g id="g_xy" transform="matrix(1,0,0,1,0,0)">
	<rect id="r_xy" x="0" y="0" width="100" height="100" style="fill:#88F"/>
	<line x1="0" y1="0" x2="97" y2="97" class="vect" style="stroke:#00F" marker-end="url(#fleche)"/>
	<text x="50" y="50" class="vect">X↷Y</text>
	<line x1="0" y1="0" x2="150" y2="0" class="axe" marker-end="url(#fleche)"/>
	<text x="154" y="0" class="axe">X</text>
      </g>
      <g id="g_yz" transform="matrix(0,0,0,0,0,0)">
	<rect id="r_yz" x="0" y="0" width="100" height="100" style="fill:#8F8"/>
	<line x1="0" y1="0" x2="97" y2="97" class="vect" style="stroke:#0F0" marker-end="url(#fleche)"/>
	<text x="50" y="50" class="vect">Y↷Z</text>
	<line x1="0" y1="0" x2="150" y2="0" class="axe" marker-end="url(#fleche)"/>
	<text x="154" y="0" class="axe">Y</text>
      </g>
      <g id="g_zx" transform="matrix(0,0,0,0,0,0)">
	<rect id="r_zx" x="0" y="0" width="100" height="100" style="fill:#F88"/>
	<line x1="0" y1="0" x2="97" y2="97" class="vect" style="stroke:#F00" marker-end="url(#fleche)"/>
	<text x="50" y="50" class="vect">Z↷X</text>
	<line x1="0" y1="0" x2="150" y2="0" class="axe" marker-end="url(#fleche)"/>
	<text x="154" y="0" class="axe">Z</text>
      </g>
      <!-- l'axe de rotation autour duquel le cube devra tourner -->
      <line id="axe" x1="0" y1="0" x2="200" y2="200" class="axe" marker-start="url(#cercle)"/>
    </g>
    <!-- <line id="debug_xy" x1="0" y1="0" x2="150" y2="0" class="axe" style="stroke:#44F" marker-end="url(#fleche)"/>
    <line id="debug_yz" x1="0" y1="0" x2="150" y2="0" class="axe" style="stroke:#4F4" marker-end="url(#fleche)"/>
    <line id="debug_zx" x1="0" y1="0" x2="150" y2="0" class="axe" style="stroke:#F44" marker-end="url(#fleche)"/> -->
  </g>
  <!-- les trois curseurs des rotations à appliquer -->
  <g id="cursors" transform="translate(0, 300)"/>
  <!-- <text id="debug" x="30" y="300">Debug</text> -->
</svg>

la feuille de style utilisée (indiquée par la première ligne <?xml-stylesheet type="text/css" href="svg_style.css" ?>) peut être celle-ci :

text {
    -o-user-select:none;
    -ms-user-select:none;
    -moz-user-select:none;
    -webkit-user-select:none;
    user-select:none;
}
text.vect {
    font-size:35px;
    text-anchor:middle;
    alignment-baseline:middle;
    text-decoration:underline;
}
text.axe {
    font-size:10px;
    text-anchor:left;
    alignment-baseline:middle;
    text-decoration:underline;
}
line.vect {
    stroke-linecap:butt;
    stroke-width:4;
}
line.axe {
    stroke:#666;
    stroke-width:1;
    stroke-dasharray:10 2 1 2;
}
line#axe {
    stroke:#F00;
    stroke-width:2;
    stroke-dasharray:10 2 1 2;
}
path {
    fill:#AAA;
    fill-opacity:0.4;
    stroke-width:0.5px;
    stroke:#000;
}

L'image étant créée, passons maintenant à la partie comportementale assurée par javascript.

L'interactivité

Pour la partie IHM la seule différence par rapport à l'exercice précédent porte sur les noms et les plages des contrôles à appliquer.

Nous utiliserons donc le fichier relatif à la partie interface réalisée précédemment que nous adapterons afin de créer des curseurs permettant de réaliser les opérations suivantes :

Il nous faudra donc une interface pourvue de 3 curseurs couvrant les plages des valeurs correspondantes et dont le tableau de déclaration est ici (à placer dans le fichier rotate.es) :

controls = {lat: {min: -90,  max: 90,  def: 40,  txt: 'latitude',  cursorStyle: 'fill:#FF0'},
	    lon: {min: -180, max: 180, def: -120,  txt: 'longitude', cursorStyle: 'fill:#0FF'},
	    rot: {min: 0,    max: 360, def: 180, txt: 'rotation',  cursorStyle: 'fill:#F0F'}};

Les fonctions 3D

Afin de nous simplifier la vie nous allons créer notre propre librairie de fonctions 3D utiles à notre application. Ces fonctions seront placées dans le fichier utils_3d.es.

Il nous faudra disposer des outils suivants :

  1. la transformation d'une matrice 4x4 en chaîne de caractères exploitable par les transformations SVG,
  2. le produit de 2 matrices 4x4,
  3. le produit d'un vecteur à 4 composantes par une matrice 4x4,
  4. le produit vectoriel de 2 vecteurs à 3 composantes,
  5. la matrice de rotation relativement à un axe quelconque,
  6. la fonction appliquant les modifications à l'axe de rotation matérialisé dans l'image SVG par l'élément de type line identifié par id="axe",
  7. la fonction appliquant les transformations à l'ensemble des éléments constituant l'objet à faire tourner.
  1. la fonction de transformation de matrice transformMatrix(matrix)

    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][3].toFixed(2)+','+matrix[1][3].toFixed(2)+')';
    }
  2. la fonction produit de matrices 4x4 prodMatrix(m1, m2)

    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;
    }
  3. la fonction produit de vecteur 4 composantes par matrice 4x4 prodMatrixVector(m, v)

    Cette fonction est donnée par le code suivant :

    function prodMatrixVector(m, v) {
        var res = new Array();
        for (var j=0; j<m.length; j++) {
    	res[j] = 0;
    	for (var i=0; i<v.length; i++) {
    	    res[j] += m[j][i] * v[i];
    	}
        }
        return res;
    }
  4. la fonction produit vectoriel prodVector(u, v)

    Cette fonction est donnée par le code suivant :

    function prodVector(u, v) {
        var pv = [u[1]*v[2] - u[2]*v[1],
    	      u[2]*v[0] - u[0]*v[2],
    	      u[0]*v[1] - u[1]*v[0]];
        return pv;
    }
  5. la fonction rotation/translation universelle getMatrix(rot, axe, tr)

    Cette fonction prends les arguments suivants :

    • rot qui indique l'angle de rotation exprimé en degrés,
    • axe qui indique le vecteur unité de l'axe de rotation (3 composantes),
    • tr qui indique le vecteur de translation (3 composantes).

    Elle donne en retour la matrice de transformation 4x4 (coordonnées homogènes)

    function getMatrix(rot, axe, tr) {
        var teta = rot * Math.PI / 180;
        var cos = Math.cos(teta);
        var sin = Math.sin(teta);
        var wX = axe[0];
        var wY = axe[1];
        var wZ = axe[2];
        var matrix = [[cos+(1-cos)*wX*wX,    (1-cos)*wX*wY-sin*wZ, (1-cos)*wX*wZ+sin*wY, tr[0]],
    		  [(1-cos)*wX*wY+sin*wZ, cos+(1-cos)*wY*wY,    (1-cos)*wY*wZ-sin*wX, tr[1]],
    		  [(1-cos)*wX*wZ-sin*wY, (1-cos)*wY*wZ+sin*wX, cos+(1-cos)*wZ*wZ,    tr[2]],
    		  [0,                    0,                    0,                    1]];
        return matrix;
    }
  6. la fonction de positionnement de l'axe de rotation à partir des latitudes et longitudes setAxe(lon, lat)

    Cette fonction prends les arguments suivants :

    • lon qui indique l'angle de longitude exprimé en degrés,
    • lat qui indique l'angle de latitude exprimé en degrés.

    Cette fonction est donnée par le code suivant :

    function setAxe(lon, lat) {
        var s = 200;
        var latR = lat * Math.PI / 180;
        var lonR = lon * Math.PI / 180;
        var axid = document.getElementById('axe');
        axeRot = [Math.sin(lonR)*Math.cos(latR), Math.sin(latR), Math.cos(lonR)*Math.cos(latR)];
        axid.setAttribute('x1', translt[0]);
        axid.setAttribute('y1', translt[1]);
        axid.setAttribute('x2', s*axeRot[0]+translt[0]);
        axid.setAttribute('y2', s*axeRot[1]+translt[1]);
        axid.setAttribute('z',  s*axeRot[2]/2+translt[2]);
    }
  7. la fonction appliquant les transformations à l'ensemble des éléments constituant l'objet à transformer DisplayMatrix(m)

    Cette fonction prend en argument m qui est un tableau associatif contenant l'ensemble des transformations initiales à appliquer au divers objets sur lesquels s'appliquera la transformation. Dans notre cas nous appliquerons 3 matrices, une par face du cube. Les clef du tableau correspondent aux id du SVG.

    Cette fonction est donnée par le code suivant :

    function rotateOrth(matrixRots) {
        // matrixProj est utilisée pour l'opération de projection. La matrice unité fera une projection de base
        //var matrixProj = [[1,0,-0.2,0], [0,1,-0.2,0], [0,0,1,0], [0,0,-0.25,1]];
        var matrixProj = [[1,0,0,0], [0,1,0,0], [0,0,-0.2,-1], [0,0,-0.25,0]];
        //var matrixProj = Munit;
        // zc est destinée à gérer la profondeur
        var zc = [];
        for (var key in matrixRots) {
    	// récupération de la matrice de rotation appliquée à la matrice de transformation de la face courante
    	var matntf = prodMatrix(getMatrix(cursors['rot'].text.textContent, axeRot, translt), matrixRots[key]);
    	var matrx = prodMatrix(matrixProj, matntf);
    	var gid = document.getElementById('g_'+key);
    	var rid = document.getElementById('r_'+key);
    	//application du de la transformation SVG
    	gid.setAttribute('transform',TransformMatrix(matrx));
    	// gestion de la vue intérieure/extérieure par signe de la composante Z du produit vectoriel (pv[2]) des vecteurs de chaque face
    	var pv = prodVector([matrx[0][0],matrx[1][0],matrx[2][0]],[matrx[0][1],matrx[1][1],matrx[2][1]]);
    	// cf donne les coordonnées du point central de la face.
    	var dx = rid.getAttribute('width');
    	var dy = rid.getAttribute('height');
    	var cf = prodMatrixVector(matrx, [dx/2,dy/2,0,1])
    	if (pv[2] < 0) {
    	    // si la face est intérieure passage en grisé par le style de la face et du texte
    	    // mémorisation de la couleur avant modification du style
    	    if (!rid._originalBackgroundColor) rid._originalBackgroundColor = rid.style.fill;
    	    gid.style.fill = '#888';
    	    gid.style.strokeOpacity = 0.2;
    	    gid.style.fillOpacity = 0.9;
    	    rid.style.fill = '#666';
    	} else {
    	    if (rid._originalBackgroundColor) {
    	    // si la face est extérieure remise en place du style initial
    	    // mais inutile de restaurer si la couleur n'est pas mémorisée c'est que le style n'est pas altéré
    		gid.style.fill = '#222';
    		gid.style.fillOpacity = 1;
    		gid.style.strokeOpacity = 1;
    		rid.style.fill = rid._originalBackgroundColor;
    	    }
    	}
    	// passage des éléments à afficher à la suite du programme pour gestion de l'empilement
    	zc.push({z:-cf[2], id:gid});
    	/****** pour le debug ******/
    	if (typeof(matrix_debug) == 'function') matrix_debug(key, matrx);
        }
        var axid = document.getElementById('axe');
        zc.push({z:-axid.getAttribute('z'), id:axid});
        // l'empilement se fera par rapport à la coordonnée croissante en Z de chaque face.
        // appel de la fonction de tri par z croissant sur le tableau zc
        var o = zc.sort(function(a, b) { return a.z - b.z; });
        for (var n in o) {
    	var gid = o[n].id;
    	gid.parentNode.appendChild(gid); 
        }
    }

Le traitement de l'action

Afin de finaliser le traitement nous devrons fournir les fonctions suivantes spécifiques à notre application et donc placées dans le fichier rotate.es :

  1. la fonction destinée à afficher des informations de debug matrix_debug(key,matrx)

    Cette fonction prends les arguments suivants :

    • key qui indique l'identifiant de l'élément visé pour le debug,
    • matrix qui indique la matrice 4x4 actuellement appliquée à l'objet à faire tourner.

    Cette fonction optionnelle est donnée par exemple par :

    function matrix_debug(key, matrx) {
        var dbgid = document.getElementById('debug_'+key);
        if (dbgid) {
    	var psx = matrx[0][0]*axeRot[0] + matrx[1][0]*axeRot[1] + matrx[2][0]*axeRot[2]; 
    	var psy = matrx[0][1]*axeRot[0] + matrx[1][1]*axeRot[1] + matrx[2][1]*axeRot[2]; 
    	var pv = prodVector([matrx[0][0],matrx[1][0],matrx[2][0]],[matrx[0][1],matrx[1][1],matrx[2][1]]);
    	var ax = pv[0]*axeRot[0]+pv[1]*axeRot[1]+pv[2]*axeRot[2];
    	var dbg = document.getElementById('debug');
    	if (dbg) dbg.textContent += ' Z:'+key+'='+pv[2].toFixed(0)+'/'+ax.toFixed(0)+'/sX'+psx.toFixed(0)+'/sY'+psy.toFixed(0);
    	var s = 130;
    	dbgid.setAttribute('x1', matrx[0][3]);
    	dbgid.setAttribute('y1', matrx[1][3]);
    	dbgid.setAttribute('x2', s*(pv[0])+matrx[0][3]);
    	dbgid.setAttribute('y2', s*(pv[1])+matrx[1][3]);
        }
    }
  2. la fonction transform() réalisant les actions depuis l'interface.

    Cette fonction contient la déclaration du tableau des matrices de transformation à appliquer aux 3 faces.

    function transform() {
        var faces = {xy: [[0,1,0,0], [-1,0,0,0], [0,0,1,0], [0,0,0,1]],
    		 yz: [[1,0,0,0], [0,0,-1,0], [0,1,0,0], [0,0,0,1]],
    		 zx: [[0,0,-1,0], [0,-1,0,0], [1,0,0,0], [0,0,0,1]]};
        setAxe(cursors['lon'].text.textContent, cursors['lat'].text.textContent);
        rotateOrth(faces);
    }
  3. J'allais oublier de vous dire d'intégrer ces variables globales qui sont nécessaires au fonctionnement de ce script :
    var axeRot  = [1,1,1];    // vecteur axe de rotation
    var translt = [50,50,0]; // vecteur de translation
    // tableau de la matrice unité (aucune transformation géométrique)
    var Munit = [[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1]];

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 :

Un navigateur moderne devrait vous montrer ici le résultat, en désespoir de cause vous pouvez essayer le lien suivant sur cette image SVG, mais je doute du résultat car un navigateur qui ne connaît pas <object> a peu de chances de connaître le SVG !