Accueil Blog ⇉ JQuery : Progress bar animée et SVG circle pendant un upload

SVG

CSS

Jquery

PHP

illustration_article

Challenge : faire une progress bar animée grâce à svg circle et JQuery de la façon la plus légère possible (et la plus simple possible car le concept de circle svg, ce n'est pas ce qui se fait de plus abordable)...
Que nous faut-il ? une page html qui contiendra la partie formulaire, une page php pour le traitement côté serveur et JQuery pour le traitement côté client. Rien que du basique.

C'est parti !


Tout d'abord, faisons un cercle simple, grâce à circle :

svg {transform: rotate(-90deg);}
#svg_cont {width: 180px;line-height:180px;margin-left:150px;}
</style>

<div id="svg_cont">
<svg viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg" style="border: red 1px solid;">
<circle cx="90" cy="90" r="80" fill="none" stroke-width="18" stroke="#FFD8A1" />
</svg>
</div>

Pour les détails de circle et viewbox , je vous renvoie à la doc où tout est clairement expliqué, mais déjà attention aux tailles, elles sont relatives, ici à la div parent, dont le principal intérêt est de "caser" l'ensemble de l'animation (le circle donc) En rentrant une valeur --n'importe laquelle-- ci-dessus, vous modifierez la taille de la div contenant le cercle de droite qui a pourtant exactement la même valeur que celui de gauche. Il est important de noter qu'en modifiant la taille de la div, vous ne touchez pas au rapport r/circonférence => ça marchera toujours.Ici nous sommes à 502.65 (2Πr = 2.80.3,14159 = 502.65) la circonférence en "pixel", et comme on va retranscrire 100% du chargement d'un fichier, 5.0265 pour le centième (soit 1%) ; bien évidemment, ce (léger) calcul est à faire en fonction du rayon de votre cercle. Comme dit plus haut, changer la taille de la div n'impacte rien, mais une circonférence erronée changera tout !

help “Pour chaque cercle, il faudra calculer sa circonférence !”


Vous avez peut-être noté le rotate: -90deg : ça "tourne" le cercle de 90° vers la gauche et revient à faire démarrer l'animation à midi, sinon, (à zéro donc) elle partirait à 3 heures, comme le montre cette figure piochée sur wiki :







Au passage on peut aussi noter que les angles Π/2 et 3Π/2 sont dans le sens contraire des aiguilles d'une montre (donc de fait, le 180° (Π) aussi, et de fait toujours, on va tourner à l'envers).
Un point simple mais fondamental pour la suite : 360° correspond ici à une valeur de 502.65, et 0° à une valeur de...0.
Voilà, nous avons grosso modo l'emplacement de notre progress bar.
A ce stade, ajoutons un autre cercle sur le premier, c'est lui qui va s'animer. Pour le maintenir à zéro, donc "invisible", nous lui mettons deux attributs de présentation dans la partie CSS, stroke-dasharray et stroke-dashoffset ; les deux sont directement liés à la représentation de notre cercle. L'un gère les pointillés, et l'autre le décalage des pointillés (c'est ce dernier qui va assurer une grosse part du travail). Ils sont tous les deux à initialiser à 502.65 (étonnement, car si on met le dashoffset à zéro, ça ne marche pas ; en fait pas si étonnant que ça, le dashoffset c'est le départ d'un pointillé : s'il est mis à la valeur de la circonférence, il décale ce pointillé de la valeur du cercle).

svg {transform: rotate(-90deg);}
#svg_path {stroke-dasharray: 502.65;stroke-dashoffset: 502.65;}
#svg_cont {width: 180px;line-height:180px;margin-left:150px;}

<div id="svg_cont">
<svg viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg" style="border: red 1px solid;">
<circle cx="90" cy="90" r="80" fill="none" stroke-width="18" stroke="#FFD8A1" />
<circle id="svg_path" cx="90" cy="90" r="80" fill="none" stroke-width="18" stroke="#714404" />
</svg>
</div>


Pour que ce soit plus clair, le dasharray est ici à 502.65 (le 0° du cercle), et le champ permet de rentrer les valeurs du dashoffset ; attention, décroissantes de 502.65 vers 0 !



Voici quelques exemples de ce qui se passe avec des valeurs différentes de 502.65 pour les deux propriétés (au passage, on peut faire des trucs très sympas):

Je vous invite à entrer une valeur dans le champ de droite pour voir l'impact sur le cercle marron (celui qui nous intéresse) ; eh bien c'est exactement ce qu'on va utiliser pour notre progress bar ; la seule (et délicate je vous l'accorde) différence est qu'il va falloir asservir la progression du cercle à celle de l'upload, nous y arrivons bientôt.
(Note importante : une valeur correspond à une position, ce n'est pas cumulatif, c'est-à-dire qu'un fichier à 2% c'est une seule position à 2%.)
Vous remarquerez aussi que le cercle de gauche, en revenant vers zéro, donne une idée du résultat ; c'est le premier problème auquel nous allons avoir à faire face : notre pourcentage de progression sera exprimé de 0 à 100, tandis que notre cercle devrait aller de 502.65 à 0.

Avant de mettre les mains dans le cambouis, voyons ça avec des valeurs basiques :


On remarque que, dans le cercle de gauche avec des valeurs CSS stroke-dash[X] à 502.65 pour un but à 0, ça marche mais c'est incompatible avec ce qu'on recevra en % ; ceci dit, c'est le principe général.
Dans le cercle du milieu, les valeurs de stroke-dash[X] sont à zéro mais le résultat est mauvais avec un but à 100, le cercle est déjà défini avant de commencer quoi que ce soit, d'où le dashoffset à 502.65 de plus haut.)
Il faut donc transformer la décroissance de 502.65 vers 0, en une progression de 0 vers 100. Pour plus de lisibilité (et de fun) les deux premiers cercles passent par un animate(), pour celui de droite qui sera la base de notre cercle final (donc gaffe) on va passer par css() qui sera rafraîchi à chaque modification de l'upload --ce qui nous évite de gérer le timing de animate() et qui va donner le résultat que vous obtenez en mettant une valeur dans le champ de droite. Cette valeur de zéro à 100 est transformée vers son équivalent de 502.65 à 0, voici la ligne en question :

 "stroke-dashoffset": 502.65-(Percentage*5.0265)

"Percentage" étant votre entrée (au final, la taille du fichier envoyé actualisée par pourcentage) ;-)

Maintenant que nous avons déblayé notre structure, et réglé la question de l'animation à l'écran, il nous reste juste à créer un petit formulaire standard du côté client, et un fichier PHP qu'on appellera avec ajax:
Ajoutons ceci à notre html :

<div>
<form id="svg_f1" enctype="multipart/form-data">
<label class="svg_inf" for="avatar">Choisissez un fichier image (<500Ko):</label><br /><br />
<input type="file" name="svg_file" accept="image/png, image/jpeg" />
<br /><br /><br />
<input type="submit"/>
</form>
</div>

Comme dit plus haut, c'est standard, attention toutefois à la valeur accept, c'est juste pour "présélectionner" le type de fichier dans l'explorateur windows, ça n'a strictement aucun impact sur le type réel des fichiers envoyés, qui sont comme d'habitude à filtrer côté client et vérifier côté serveur.

help “TOUJOURS vérifier ce qu'envoie un utilisateur”


Du côté serveur justement, les vérifications d'usage :

 if(!isset($_FILES['svg_file'])) {
//Le ficher excède la taille autorisée du php.ini ou n'existe pas...
}
if (empty($_FILES['svg_file']['tmp_name']) and (!is_uploaded_file($_FILES['svg_file']['tmp_name'])))
{
//Le fichier n'existe pas ou n'a pas été chargé...

} etc...

La gestion va se borner à renvoyer deux ou trois infos sur le fichier temporaire uploadé grâce aux bons soins de is_uploaded_file() ou le message d'erreur adhoc en cas de problème; une fois traité, il est supprimé purement et simplement sans avoir été enregistré. Ne pas oublier de bien renseigner le premier index de $_FILES avec le nom du formulaire.

La gestion du formulaire est strictement standard, et maintenant que nous sommes fin prêts, passons à l'appel Ajax qui va animer notre progress bar. C'est un appel normal dans le cadre d'un chargement de fichier, sauf l'utilisation d'un outil précieux : XMLHttpRequest(), familier à ceux qui refusent obstinément de quitter le JS pur et qu'on utilise sans y penser avec JQuery puisqu'il est instancié d'office avec $.ajax() ;-)
Nous allons donc récupérer notre objet XMLHttpRequest directement sous la forme var xhr = new XMLHttpRequest et travailler avec la variable xhr :

Dernière ligne droite


L'idée :
Tout d'abord, nous le lions avec un objet de type ajaxSettings (doc ici)

var svgXhr = $.ajaxSettings.xhr();

(ne vous inquiétez pas je vous donne le code complet en-dessous -)
Nous associons ensuite cet objet avec la propriété upload de XMLHttpRequest pour récupérer un objet de type XMLHttpRequestUpload

svgXhr.upload

qui va, lui, permettre d'observer l'upload via un listener d’événement
avec cette syntaxe :

target.addEventListener('type', listener, [options]);

qui va nous donner dans notre cas :

 svgXhr.upload.addEventListener('progress',progressBar, false);



Il y a d'autres addEventListener :

xhr.addEventListener("progress", updateProgress); 
xhr.addEventListener("load", transferComplete);
xhr.addEventListener("error", transferFailed);
xhr.addEventListener("abort", transferCanceled).



Bref, fondamentalement, le plus dur est fait. Il y a deux façons de gérer notre function (elles reviennent au même c'est juste une question de confort) la première donne ici :

 xhr: function() {
var svgXhr = $.ajaxSettings.xhr();
if(svgXhr.upload){
svgXhr.upload.addEventListener('progress',progressBar, false);
}
return svgXhr;
},


Et là, si vous avez suivi, vous êtes en train de vous dire "mais... et la fonction progressBar ????"
On y vient, elle est déconcertante de facilité justement.
Donc, cette fonction récupère l'événement courant (e) pour faire original) ; elle vérifie qu'il est gérable (lenghtComputable), puis définit son poids total e.total et ce qui est actuellement déjà chargé e.loaded ; simplissime comme promis ; dans les faits ça donne ça :

function progressBar(e){
if(e.lengthComputable){
var max = e.total;
var current = e.loaded;

et si vous n'avez pas oublié ma variable Percentage de tout à l'heure, elle est tout simplement :

var Percentage = Math.ceil((current * 100)/max);



La deuxième façon consiste tout simplement à inclure la fonction progressBar dans la fonction xhr (déjà dans votre script en principe) :

xhr: function() {
var svgXhr = $.ajaxSettings.xhr();
if(svgXhr.upload){
svgXhr.upload.addEventListener('progress',function(e) {
if(e.lengthComputable){
var max = e.total;
var current = e.loaded;
var Percentage = Math.ceil((current * 100)/max);
}
}, false);
}
return svgXhr;
},

C'est vraiment comme on préfère...

Vérifions que ça marche :


Non seulement l'upload marche très bien mais les valeurs sont cohérentes et la correction particulièrement satisfaisante (c'est le moment de se jeter des fleurs). Dans cet article je me borne à "progress" mais il est conseillé d'inclure les autres addEventListener.
Çe test-ci m'a pris quelques heures ;-)


La plupart du temps, sur les gros transferts, les connexions étant parasitées, il pourra y avoir des pouillèmes de différence après la virgule, pouillèmes qu'on ne considérera pas comme étant rédhibitoires.

Il ne reste plus qu'à ajouter

$("#svg_path").css({
"stroke-dashoffset": 502.65-(Percentage*5.0265)
});

à la fonction progressBar pour donner à votre cercle l'animation en fonction de ce qui est réellement chargé.
(J'évoque ce point après avoir vu un genre de plugin qui corrélait l'upload à une espèce de setInterval, pour le coup complètement aléatoire, et qui d'ailleurs ne marchait pas vraiment).
Et vient l'heure de la déco : je vous renvoie à la doc pour des propriétés sympas qui peuvent être exploitées. Voici ce qu'on peut faire vite fait : (je ne suis pas un as de la décoration, mais j'en suis assez content)



j'espère que ce petit papier vous aura plus ; j'ai limité à 500Ko (pour ma bande), aussi ceux qui veulent tester les différences de vitesse sur de petits fichiers images peuvent réduire (artificiellement) leur débit en allant sur les outils de développement ([F12] ou [Ctrl+Shift+i], puis "network", (éventuellement "disable cache") puis descendre les valeurs de "online").
Vos fichiers ne sont pas stockés, ils sont détruits directement une fois que le fichier PHP a renvoyé ses infos ;-)
Chose promise chose due : le fichier php pèse 1Ko, le fichier du dernier exemple 5Ko, (formulaire + circle + retours) ce qui fait un total de 6 Ko.
Une erreur étant, par essence, humaine, n'hésitez pas à me signaler tout ce qui vous paraît louche ;-)
Comme d'habitude, je reste à votre disposition, de préférence via le contact de ma page (gb-net.fr), n'hésitez pas !



Modifié le 13-11-2019
Article précédent


Cookies et navigation : la 11è plaie d'Egypte
Article suivant


Formulaire drag and drop avec preview

Commentaires

Pas encore de commentaire

Publier un commentaire :



capcha   



Raccourcis : php css html sql js img

Prévisualiser
GB-Net.fr 2020



fleche haut