Réaliser une horloge avec CSS3 et un peu de JavaScript

Cet article va vous présenter étape par étape les différentes techniques CSS (et un peu de JavaScript) permettant de mettre en place une horloge.

7 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Le code de base

Le balisage HTML est tout ce qu'il y a de plus simple :

 
Sélectionnez
<div id="horloge>
    <div id="heure"></div>
    <div id="minute"></div>
    <div id="seconde"></div>
    <div id="centre"></div>
</div>

Inutile de s'y attarder plus que nécessaire : la <div> #horloge correspond au conteneur, #heure, #minute et #seconde aux aiguilles et #centre à l'axe de rotation.

I-A. Styles de l'horloge

Bien sûr, si on affiche la page telle quelle, on ne va pas voir grand-chose !
Ajoutons donc quelques styles de base :

 
Sélectionnez
#horloge{
    height: 400px;
    margin: 40px auto;
    position: relative;
    width: 400px;
    border:10px solid #3A5486;
    background:url(http://www.developpez.net/template/images/logo.png) no-repeat scroll center 25% #FFFFFF;
    box-shadow: 0 0 40px #8080A0, 0 0 50px 10px #CCCCCC inset;
}

Tout d'abord, nous mettons un border-radius à 50% et une bordure bleutée à l'horloge afin de lui donner une forme arrondie.

Quelles que soient les dimensions d'un élément, fixer la valeur de border-radius à 50% permet d'obtenir une forme arrondie.
En revanche, si on fixe la taille d'un élément (par exemple 400px), qu'on ajoute une bordure et qu'on fixe la valeur de border-radius à la moitié de la taille de l'élément (200px), l'arrondi ne tiendra pas compte de la bordure elle-même.
Il est possible de corriger ce comportement en utilisant la propriété box-sizing: border-box (attention, cette propriété peut nécessiter d'être préfixée).

Nous allons devoir positionner les différents éléments internes de l'horloge par rapport au conteneur #horloge. Pour cela, il est nécessaire de donner à la propriété position une valeur autre que static.

Pour notre exemple, un simple position: relative; sera suffisant et permettra de centrer l'horloge avec margin: auto; mais il est aussi possible d'utiliser les valeurs absolute ou fixed si l'on souhaite placer l'horloge à un endroit précis de la page.

On rajoute une marge horizontale avec la valeur 40px auto afin de décoller l'horloge du titre et de la centrer horizontalement.
On ajoute aussi quelques effets sur la bordure à l'aide de box-shadow.

Pour peaufiner encore le rendu, nous allons ajouter quelques styles avec le pseudoélément :before :

 
Sélectionnez
#horloge:before{
    border-radius:50%;
    box-shadow: -2px -2px 5px #000000 inset, -2px 2px 5px #000000 inset, 2px -2px 5px #000000 inset, 2px 2px 5px #000000 inset;
    position: absolute;
    content: '';
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
}

Tout d'abord, nous créons un contenu vide (content: '') nécessaire pour pouvoir appliquer les styles.
Nous adaptons les dimensions à celles de l'horloge avec la position absolute et une valeur 0 pour les propriétés left, top, right et bottom.
Enfin, nous appliquons une ombre interne (valeur inset de box-shadow) et arrondissons l'ensemble (border-radius: 50%) pour donner un léger effet de relief.

Voici le rendu actuel (voir en ligne) :

Code actuel
CacherSélectionnez

I-B. Les aiguilles

Tout cela est parfait pour le cadre de l'horloge, mais les aiguilles et le centre, bien que présents dans le code HTML, n'apparaissent pas. Nous allons donc leur affecter les styles permettant de les visualiser.

 
Sélectionnez
#seconde{
    position: absolute;
    left: 198px;
    top: 25px;
    height: 180px;
    border: 2px solid rgba( 110,150,180,0.8);
    border-radius: 50%;
}
#minute{
    position: absolute;
    left: 196px;
    top: 55px;
    height: 150px;
    border: 4px solid rgba( 80,150,200,0.8);
    border-radius: 50%;
}
#heure{
    position: absolute;
    left: 194px;
    top: 100px;
    height: 100px;
    border: 6px solid rgba( 10,50,128,0.8);
    border-radius: 50%;
}
#centre{
    position: absolute;
    left: 194px;
    top: 194px;
    border: 6px solid rgba(255,255,255,0.6);
    border-radius: 50%;
}

Tout d'abord, nous affectons à tous ces éléments un position: absolute afin de pouvoir les positionner par rapport à leur plus proche parent positionné (ce qui explique le position: relative de #horloge).
On affecte ensuite une hauteur et une bordure adaptée à chaque aiguille de façon à pouvoir les visualiser. Comme sur une horloge usuelle, l'aiguille des heures sera courte et large, celle des minutes plus longue et un peu moins large et celle des secondes sera longue et fine.
La couleur des bordures de chaque aiguille est en nuance de bleu (pour s'adapter au style général de developpez.com) légèrement transparent pour pouvoir visualiser les chevauchements. On choisit donc un mode de couleur en rgba() (red, green, blue, alpha) apparu en CSS 3.
En fonction des éléments de hauteur et de bordure, nous positionnons chaque aiguille au centre de l'horloge.
Enfin, nous donnons un effet bombé aux aiguilles avec un arrondi de bordure à 50 %.

Nous avons donné à l'horloge des dimensions carrées, ainsi, le border-radius: 50% a pour effet de rendre l'élément rond.
Dans le cas des aiguilles, comme ces éléments n'ont pas de largeur puisqu'ils sont vides, le border-radius: 50% produira des éléments bombés, bien adaptés pour des aiguilles d'horloge.

Enfin, nous appliquons le même principe pour le centre : positionnement absolu, bordure semi-opaque et arrondi de bordure pour créer un cercle (toutes les bordures ayant la même épaisseur).

Nous pouvons constater que plusieurs éléments possèdent des caractéristiques identiques.
Plutôt que répéter ces règles CSS, nous pouvons créer une classe spécifique pour ne les déclarer qu'une fois.
Nous créons donc une classe .rond comme suit :
 
Sélectionnez
.rond{
    position: absolute;
    border-radius: 50%;
}

Notre horloge ressemble maintenant à ceci (voir en ligne) :

Code de l'exemple
CacherSélectionnez

I-C. Les repères de l'horloge

Habituellement, une horloge possède différents repères pour matérialiser les heures (voire les minutes).
Nous allons donc mettre en place ces différents repères. Nous nous contenterons des heures, un repère par minute s'avérant surcharger inutilement l'affichage.

Nous ajoutons donc de nouveaux éléments dans le code HTML :

 
Sélectionnez
<div id="horloge">
    <div id="heure" class="rond"></div>
    <div id="minute" class="rond"></div>
    <div id="seconde" class="rond"></div>
    <div id="centre" class="rond"></div>
    <div class="sep" id="h0"></div>
    <div class="sep" id="h1"></div>
    <div class="sep" id="h2"></div>
    <div class="sep" id="h3"></div>
    <div class="sep" id="h4"></div>
    <div class="sep" id="h5"></div>
    <div class="sep" id="h6"></div>
    <div class="sep" id="h7"></div>
    <div class="sep" id="h8"></div>
    <div class="sep" id="h9"></div>
    <div class="sep" id="h10"></div>
    <div class="sep" id="h11"></div>
</div>

Nous appliquons à chacun de ces repères un style générique via la classe .sep :

 
Sélectionnez
.sep{
    position: absolute;
    height: 10px;
    width: 10px;
    text-align: center;
    line-height: 10px;
    top: 195px;
    left: 195px;
    color: #3A5486;
}
.sep:before{
    content: '&#8226;';
}

Tout d'abord, nous positionnons les marqueurs en absolute. Cependant, cette règle n'est présente ici que pour l'évoquer et il n'est pas nécessaire de la conserver, nous allons juste ajouter le sélecteur .sep à la définition de la classe .rond :

 
Sélectionnez
.rond, .sep{
    position: absolute;
    border-radius: 50%;
}

Certes, cela aura pour effet d'arrondir les bordures des marqueurs, mais comme ceux-ci ne possèdent pas de bordure, ce n'est pas bien gênant.

Comme ces éléments auront pour contenu un symbole de rond dont on ne peut garantir les dimensions exactes, nous donnons des dimensions fixes aux repères.
Pour des raisons similaires, nous centrons horizontalement (text-align) et verticalement le contenu. Pour le centrage vertical, nous affectons à la propriété line-height une valeur égale à la hauteur de l'élément, ainsi, comme le contenu est aligné par défaut au centre de la hauteur de ligne, le centrage vertical se fera automatiquement.
Enfin, nous affectons une couleur adaptée au thème de l'horloge et nous positionnons ces marqueurs au centre de l'horloge.

Pour finir, le contenu est généré par CSS avec le pseudoélément :before.
Ce choix est arbitraire, nous aurions aussi bien pu l'intégrer dans le code HTML. Néanmoins, il ne s'agit pas de contenu pertinent relevant de HTML et surtout, nous déciderons par la suite de générer ces marqueurs via JavaScript, le pseudoélément permettra alors de simplifier les choses.

C'est parfait, nous avons bien nos douze repères, mais pour l'instant, ils restent superposés au centre de l'horloge, ce qui n'a pas beaucoup d'intérêt…

Nous allons donc les positionner à des emplacements adéquats en utilisant leurs identifiants et des propriétés de transformation.

Comme souvent, il existe plusieurs méthodes possibles pour atteindre un même but. Ici, nous aurions pu simplement calculer la position souhaitée sur l'horloge et ajuster les propriétés top et left. Mais ceci aurait été inutilement fastidieux et peu adaptatif. Vous allez voir que des transformations CSS sont plus faciles à utiliser dans ce genre de situation et permettent de s'adapter à des modifications futures des dimensions assez facilement.

Nous allons devoir appliquer à chaque repère une translation de 190 pixels afin de les positionner légèrement en dessous de la bordure interne et une rotation d'une valeur croissante de 0 à 330 degrés par pas de 30 degrés.

La règle CSS correspondante sera donc :

 
Sélectionnez
rotate(n*30deg) translate(0px, -190px);

Ici, n correspond au rang du repère (de 0 à 11) et nous passons deux valeurs à translate() pour indiquer la translation sur les deux axes du plan.

En CSS, l'ordre dans lequel sont appliquées les transformations est celui où elles apparaissent dans le code.
Ainsi, rotate() translate() ne donnera pas le même résultat que translate() rotate().
En effet, les transformations se font à partir d'un point d'origine (par défaut le centre de l'élément) qui est lié à l'élément auquel s'applique la transformation.
De ce fait, si nous appliquons d'abord la translation, le point d'origine sera translaté en même temps et donc la rotation se fera autour du centre de l'élément (ce qui, pour un rond, n'a pas beaucoup d'intérêt).
À l'inverse, si nous appliquons d'abord la rotation, cela n'aura pas d'effet visuel, mais les axes horizontaux et verticaux auront eux aussi pivoté et en appliquant la translation, celle-ci se fera le long des axes inclinés, déplaçant l'élément à l'emplacement souhaité.
Il est possible de définir la position de l'origine de la transformation (propriété transform-origin), mais cela ne nous est pas utile pour les repères.

En appliquant la règle précédente à chaque repère, nous obtenons le code suivant (notez l'utilisation de préfixes vendeur) :

 
Sélectionnez
#h0{
    -webkit-transform: translate(0px, -190px);
    -o-transform: translate(0px, -190px);
    transform: translate(0px, -190px);
}
#h1{
    -webkit-transform: rotate(30deg) translate(0px, -190px);
    -o-transform: rotate(30deg) translate(0px, -190px);
    transform: rotate(30deg) translate(0px, -190px);
}
#h2{
    -webkit-transform: rotate(60deg) translate(0px, -190px);
    -o-transform: rotate(60deg) translate(0px, -190px);
    transform: rotate(60deg) translate(0px, -190px);
}
#h3{
    -webkit-transform: rotate(90deg) translate(0px, -190px);
    -o-transform: rotate(90deg) translate(0px, -190px);
    transform: rotate(90deg) translate(0px, -190px);
}
#h4{
    -webkit-transform: rotate(120deg) translate(0px, -190px);
    -o-transform: rotate(120deg) translate(0px, -190px);
    transform: rotate(120deg) translate(0px, -190px);
}
#h5{
    -webkit-transform: rotate(150deg) translate(0px, -190px);
    -o-transform: rotate(150deg) translate(0px, -190px);
    transform: rotate(150deg) translate(0px, -190px);
}
#h6{
    -webkit-transform: rotate(180deg) translate(0px, -190px);
    -o-transform: rotate(180deg) translate(0px, -190px);
    transform: rotate(180deg) translate(0px, -190px);
}
#h7{
    -webkit-transform: rotate(210deg) translate(0px, -190px);
    -o-transform: rotate(210deg) translate(0px, -190px);
    transform: rotate(210deg) translate(0px, -190px);
}
#h8{
    -webkit-transform: rotate(240deg) translate(0px, -190px);
    -o-transform: rotate(240deg) translate(0px, -190px);
    transform: rotate(240deg) translate(0px, -190px);
}
#h9{
    -webkit-transform: rotate(270deg) translate(0px, -190px);
    -o-transform: rotate(270deg) translate(0px, -190px);
    transform: rotate(270deg) translate(0px, -190px);
}
#h10{
    -webkit-transform: rotate(300deg) translate(0px, -190px);
    -o-transform: rotate(300deg) translate(0px, -190px);
    transform: rotate(300deg) translate(0px, -190px);
}
#h11{
    -webkit-transform: rotate(330deg) translate(0px, -190px);
    -o-transform: rotate(330deg) translate(0px, -190px);
    transform: rotate(330deg) translate(0px, -190px);
}

Voici à quoi ressemble désormais notre horloge (voir en ligne) :

Code de l'exemple
CacherSélectionnez

Nous verrons plus tard comment générer ce code répétitif avec quelques lignes de JavaScript, mais pour l'instant, seul CSS est suffisant.

II. Animation des aiguilles

Nous avons maintenant une base assez aboutie pour notre horloge. Sauf que pour l'instant, elle reste bloquée à midi… nous allons lui ajouter les piles pour qu'elle puisse fonctionner !

Pour que notre horloge fonctionne, nous allons appliquer des animations aux aiguilles.
Les animations CSS se font à l'aide de deux éléments distincts : la définition des étapes de l'animation et les propriétés de l'animation.
Les différentes étapes sont indépendantes des éléments auxquels s'appliquera l'animation. On les définit en dehors de tout sélecteur avec les règles @keyframes (ces règles CSS précédées d'un @ sont appelées at-rules). Il est nécessaire d'utiliser un préfixe pour les navigateurs de la famille webkit (@-webkit-keyframes) et presto (anciennes versions d'Opera, @-o-keyframes).
Nous allons donc créer une animation permettant aux aiguilles de faire un tour complet autour de leur axe de rotation (le centre de l'horloge) :

 
Sélectionnez
@-webkit-keyframes tour{
    from{
        -webkit-transform: rotate(0deg);
    }
    to{
        -webkit-transform: rotate(360deg);
    }
}
@-o-keyframes tour{
    from{
        -o-transform: rotate(0deg);
    }
    to{
        -o-transform: rotate(360deg);
    }
}
@keyframes tour{
    from{
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    to{
        -webkit-transform: rotate(360deg);
        transform: rotate(360deg);
    }
}

Une animation est définie par un nom (ici tour) que l'on indique après le mot-clé @keyframes, puis on ouvre un bloc de déclarations.
À l'intérieur de ce bloc, nous allons trouver les différentes étapes de l'animation, elles sont définies par un pourcentage de la durée de l'animation. Les termes from et to correspondent respectivement à 0% et 100%.
Dans notre cas, nous souhaitons juste définir un tour complet et uniforme, il est donc inutile de préciser des étapes intermédiaires.

Dans le code ci-dessus, l'utilisation des préfixes peut sembler étonnante, notamment la répétition de -webkit-transform dans la partie non préfixée.
Cela est dû à différents navigateurs utilisant le moteur webkit : certains (Opera 15+) ont besoin du préfixe pour @keyframes et transform alors que d'autres (Chrome) reconnaissent @keyframes sans préfixe, mais transform avec préfixe.
Enfin, il faut prévoir deux versions de préfixes pour Opera : -o- pour les versions 12 et antérieures, -webkit- pour les versions 15 et ultérieures, Opera utilisant depuis cette version le moteur webkit.

Vous remarquerez que les étapes de l'animation ne définissent que des points-clés et la valeur de certaines propriétés pour chaque point-clé. Les détails liés à l'animation sont définis individuellement pour chaque élément devant utiliser cette animation.

Maintenant que l'animation est définie, nous allons l'appliquer aux aiguilles avec la propriété animation :

 
Sélectionnez
#seconde{
    -webkit-animation: tour 60s steps(60, end) infinite;
    -webkit-transform-origin: 50% 175px;
    -o-animation: tour 60s steps(60, end) infinite;
    -o-transform-origin: 50% 175px;
    animation: tour 60s steps(60, end) infinite;
    transform-origin: 50% 175px;
}
#minute{
    -webkit-animation: tour 3600s steps(60, end) infinite;
    -webkit-transform-origin: 50% 145px;
    -o-animation: tour 3600s steps(60, end) infinite;
    -o-transform-origin: 50% 145px;
    animation: tour 3600s steps(60, end) infinite;
    transform-origin: 50% 145px;
}
#heure{
    -webkit-animation: tour 43200s linear infinite;
    -webkit-transform-origin: 50% 100px;
    -o-animation: tour 43200s linear infinite;
    -o-transform-origin: 50% 100px;
    animation: tour 43200s linear infinite;
    transform-origin: 50% 100px;
}

La définition de l'animation contient quatre valeurs.
Tout d'abord, le nom de l'animation, afin de pouvoir la lier au @keyframes correspondant.
Ensuite, la durée de l'animation. Pour notre horloge, nous voulons que l'animation (un tour complet), dure une minute (60s) pour les secondes, une heure (3600s) pour les minutes et 12 heures (43200s) pour les heures.
La troisième valeur correspond à une fonction d'assouplissement (appelée easing). Pour les secondes et les minutes, nous souhaitons que l'animation se fasse par paliers (avec 60 paliers correspondant aux 60 secondes ou minutes) et que l'animation se fasse à la fin de chaque palier, d'où la valeur steps(60, end) ; pour les heures, une animation linéaire est suffisante.
La valeur infinite permet d'indiquer que l'on souhaite que l'animation soit répétée indéfiniment.

Enfin, nous fixons le point d'origine de la transformation pour chaque élément afin qu'elle corresponde au centre de l'horloge.

Voici à quoi ressemble désormais notre horloge (voir en ligne) :

Code de l'exemple
CacherSélectionnez

III. Mise à l'heure

Notre horloge est désormais quasiment fonctionnelle. Sauf qu'en l'état, il s'agit plus d'un chronomètre que d'une horloge puisqu'elle ne donne l'heure exacte que si vous affichez la page à midi ou minuit.
Il va donc falloir trouver un système pour la mettre automatiquement à l'heure au moment de l'affichage de la page.

Malheureusement, CSS ne permet pas ce genre de manipulation. JavaScript étant le seul langage côté client capable de manipuler les dates (et donc les heures), nous sommes obligés de l'utiliser pour l'initialisation de l'horloge.
Mais rassurez-vous : nous l'utiliserons uniquement pour ajuster des propriétés CSS !

Il serait possible aussi de gérer la mise à l'heure côté serveur à la création de la page (en PHP par exemple).
Pour cela, il suffirait de créer dynamiquement une feuille de style prenant en compte les modifications que nous allons faire en JavaScript.
Cependant, cela risque d'être moins précis, en effet, il y aura un léger décalage entre le moment où les styles sont générés et le moment où ils seront appliqués, car il faut compter le temps de transfert entre le serveur et le navigateur. D'autre part, il faudra tenir compte d'un éventuel décalage horaire en fonction de la localisation ou des réglages du serveur.

Il existe une propriété d'animation qui va nous être utile pour la mise à l'heure et que nous n'avons pas encore utilisée, la propriété animation-delay.
Cette propriété permet de prévoir un délai avant que l'animation ne commence.

Cependant, cette fonctionnalité ne semble pas très adaptée à notre cas au premier abord, car ce que nous souhaiterions, ce n'est pas d'attendre avant de lancer l'animation, mais au contraire, faire comme si elle avait déjà commencé depuis un temps donné…
Heureusement, cela est possible avec CSS en indiquant une valeur négative à animation-delay ce qui a pour effet de lancer l'animation immédiatement, mais à un état qui correspond à celui qu'aurait l'animation si elle avait été lancée au moment indiqué par le délai. C'est exactement ce qu'il nous faut.

Nous allons donc décaler l'animation des secondes du nombre de secondes de l'heure actuelle, celle des minutes de 60 fois le nombre de minutes actuelles plus du nombre de secondes et celle des heures de 3600 fois le nombre d'heures (moins 12 si nécessaire) plus 60 fois le nombre de minutes plus le nombre de secondes.
Ce qui donne le code JavaScript suivant :

 
Sélectionnez
var tt = new Date();
/* On applique pour chaque délai les préfixes utiles */
document.getElementById('seconde').style.webkitAnimationDelay = -(tt.getSeconds()) + 's';
document.getElementById('minute').style.webkitAnimationDelay = -(tt.getMinutes()*60 + tt.getSeconds()) + 's';
document.getElementById('heure').style.OAnimationDelay = -((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's';
document.getElementById('seconde').style.OAnimationDelay = -(tt.getSeconds()) + 's';
document.getElementById('minute').style.OAnimationDelay = -(tt.getMinutes()*60 + tt.getSeconds()) + 's';
document.getElementById('heure').style.webkitAnimationDelay = -((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's';
document.getElementById('seconde').style.animationDelay = -(tt.getSeconds()) + 's';
document.getElementById('minute').style.animationDelay = -(tt.getMinutes()*60 + tt.getSeconds()) + 's';
document.getElementById('heure').style.animationDelay = -((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's';

Voici le résultat final (voir en ligne) :

Code de l'exemple
CacherSélectionnez

IV. Aller plus loin avec JavaScript

En l'état actuel, le code est pleinement fonctionnel.
Le reste de l'article va présenter des techniques complémentaires en JavaScript, elle n'est donc pas essentielle à la mise en œuvre des exemples et sera surtout utile aux développeurs JavaScript.

Puisque nous avons été obligés d'utiliser JavaScript, autant s'en servir pour alléger le code.

Dans cette partie, nous allons utiliser un script utilitaire pour la recherche de préfixes vendeur : PrefixFinder qui permettra d'éviter de multiplier des codes identiques pour les préfixes vendeur.

IV-A. Générer les repères en JavaScript

Vous aurez probablement constaté que le code HTML des repères de l'horloge ainsi que les règles de transformation CSS liées produisent un code assez lourd et redondant.
Nous allons pouvoir automatiser leur création en JavaScript et alléger le code de la page.

Pour cela, nous allons utiliser une boucle pour créer chaque repère et lui affecter la transformation adaptée en fonction de l'indice du compteur.
Voici le script commenté :

 
Sélectionnez
// On initialise PrefixFinder
var prefixer = new PrefixFinder(),
    // On stocke la propriété préfixée pour le navigateur
    jsTransform = prefixer.getPrefixedProp('transform').js,
    // On crée un élément correspondant au repère que l'on clonera à chaque itération
    $sep = document.createElement('div'),
    // La variable contenant l'élément cloné à chaque itération
    $sepactu,
    // On stocke l'élément horloge
    $horloge = document.getElementById('horloge');
// Tous les repères auront la classe CSS 'sep'
$sep.className = 'sep';
// On crée les douze séparateurs
for(var i = 0; i < 12; i++){
    // On clone le repère générique
    $sepactu = $sep.cloneNode(true);
    // On affecte le style en fonction de l'indice de l'itération
    $sepactu.style[jsTransform] = 'rotate(' + (i*30) + 'deg) translateY(-190px)';
    // On insère le repère dans l'horloge
    $horloge.appendChild($sepactu);
}

Comme vous pouvez le constater, nous ne nous préoccupons pas du contenu des repères : le pseudoélément :before étant là pour ça.

Vous pouvez voir ci-dessous le code complet de la page (le rendu étant identique au précédent, nous ne l'afficherons plus).
Voir l'exemple en ligne.

 
CacherSélectionnez

IV-B. Générer les propriétés préfixées en JavaScript

De la même façon, le code CSS des propriétés préfixées rend le code assez lourd. Autant utiliser JavaScript et PrefixFinder pour les générer.
Cela permettra de plus d'intégrer la propriété animation-delay directement.

Voici le code JavaScript que nous obtenons (voir le résultat en ligne) :

 
Sélectionnez
var prefixer = new PrefixFinder(),
    tt = new Date(),
    jsTransform = prefixer.getPrefixedProp('transform').js,
    jsAnim = prefixer.getPrefixedProp('animation').js,
    $sep = document.createElement('div'),
    $sepactu,
    $horloge = document.getElementById('horloge'),
    secondeCSS = document.getElementById('seconde').style,
    minuteCSS = document.getElementById('minute').style,
    heureCSS = document.getElementById('heure').style;
$sep.className = 'sep';
secondeCSS[jsAnim] = 'tour 60s -' + (tt.getSeconds()) + 's steps(60, end) infinite';
secondeCSS[jsTransform + 'Origin'] = '50% 175px';
minuteCSS[jsAnim] = 'tour 3600s -' + (tt.getMinutes()*60 + tt.getSeconds()) + 's steps(60, end) infinite';
minuteCSS[jsTransform + 'Origin'] = '50% 145px';
heureCSS[jsAnim] = 'tour 43200s -' + ((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's linear infinite';
heureCSS[jsTransform + 'Origin'] = '50% 100px';
for(var i = 0; i < 12; i++){
    $sepactu = $sep.cloneNode(true);
    $sepactu.style[jsTransform] = 'rotate(' + (i*30) + 'deg) translateY(-190px)';
    $horloge.appendChild($sepactu);
}

Il est inutile de stocker à la fois la syntaxe de transform et de transform-origin, il suffit pour cette dernière d'ajouter la chaine Origin à la première.

IV-C. Générer les étapes en JavaScript

Nous pouvons enfin supprimer le code redondant de @keyframes pour le générer en JavaScript.

Dans le cas de l'horloge, il n'est pas forcément opportun de générer les étapes en JavaScript, car le code permettant de le faire est assez complexe.
Cependant, cela reste une technique qui pourra être utile dans d'autres cas qu'il nous a semblé utile de présenter.

La syntaxe sera légèrement complexe et nous ne pourrons pas utiliser PrefixFinder. En effet, les at-rules ne s'appliquent pas à un élément spécifique, mais sont globales au niveau de la page, il est donc impossible de tester si un élément possède une propriété @keywords.

La technique consiste donc à créer une nouvelle feuille de style et d'essayer d'y insérer la règle voulue.

Techniquement, il serait possible d'utiliser une feuille de style existante pour y insérer les at-rules. Cependant, rien ne permet de présumer qu'il existe bien au moins une feuille de style au moment d'exécuter le script, c'est pourquoi il est préférable d'en créer une plutôt que de risquer de générer une erreur.

Le code pour créer une feuille de style est le suivant :

 
Sélectionnez
var styleElement = document.createElement('style');
document.getElementsByTagName('script')[0].parentNode.insertBefore(document.getElementsByTagName('script')[0], styleElement);

Tout d'abord, nous créons une nouvelle balise <style>.
Pour pouvoir y insérer des règles CSS, il faut qu'elle soit présente dans le document. Nous l'intégrons donc avant la première balise <script>.

Il semblerait plus logique d'insérer la nouvelle balise dans le <head> à l'aide de getElementsByTagName('head')[0] et appendChild(), mais comme précédemment, rien ne nous permet de présager qu'il y a bien une balise <head> dans le document (cette balise est optionnelle selon les spécifications) en revanche, nous sommes certains qu'il existe une balise <script> : celle contenant le script qui est en train d'être exécuté.

Pour intégrer nos règles CSS, nous utilisons la méthode insertRule() :

 
Sélectionnez
var cssTransform = prefixer.getPrefixedProp('transform').css;
try{
    styleElement.sheet.insertRule('@keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
}
catch(e){
    try{
        styleElement.sheet.insertRule('@' + prefixer.prefixe.css + 'keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
    }
    catch(e){
        console.log('Pas d\'animation');
    }
}

Si l'on essaye de définir une règle dont la syntaxe n'est pas valide à l'aide de insertRule(), le navigateur renvoie une erreur, c'est pour cela que nous sommes obligés d'utiliser des blocs try catch.

Voici le code JavaScript complet (voir le résultat en ligne) :

 
Sélectionnez
var prefixer = new PrefixFinder(),
    tt = new Date(),
    jsTransform = prefixer.getPrefixedProp('transform').js,
    cssTransform = prefixer.getPrefixedProp('transform').css,
    jsAnim = prefixer.getPrefixedProp('animation').js,
    $sep = document.createElement('div'),
    $sepactu,
    $horloge = document.getElementById('horloge'),
    secondeCSS = document.getElementById('seconde').style,
    minuteCSS = document.getElementById('minute').style,
    heureCSS = document.getElementById('heure').style;
var styleElement = document.createElement('style');
document.getElementsByTagName('script')[0].parentNode.insertBefore(styleElement, document.getElementsByTagName('script')[0]);
try{
    styleElement.sheet.insertRule('@keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
}
catch(e){
    try{
        styleElement.sheet.insertRule('@' + prefixer.prefixe.css + 'keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
    }
    catch(e){
        console.log('Pas d\'animation');
    }
}
$sep.className = 'sep';
secondeCSS[jsAnim] = 'tour 60s -' + (tt.getSeconds()) + 's steps(60, end) infinite';
secondeCSS[jsTransform + 'Origin'] = '50% 175px';
minuteCSS[jsAnim] = 'tour 3600s -' + (tt.getMinutes()*60 + tt.getSeconds()) + 's steps(60, end) infinite';
minuteCSS[jsTransform + 'Origin'] = '50% 145px';
heureCSS[jsAnim] = 'tour 43200s -' + ((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's linear infinite';
heureCSS[jsTransform + 'Origin'] = '50% 100px';
for(var i = 0; i < 12; i++){
    $sepactu = $sep.cloneNode(true);
    $sepactu.style[jsTransform] = 'rotate(' + (i*30) + 'deg) translateY(-190px)';
    $horloge.appendChild($sepactu);
}

IV-D. Optimisation et gestion des erreurs

Notre code est maintenant fonctionnel, mais nous pouvons encore l'améliorer légèrement.
En effet, de nombreuses variables sont déclarées et elles encombrent le contexte global alors que nous n'avons besoin du code qu'une seule fois.
Nous allons donc les confiner dans un contexte spécifique en utilisant la syntaxe d'expression de fonction appelée immédiatement (IIFEImmediately Invoked Function Expressions). Pour cela, nous définissons une fonction anonyme entourée de parenthèses que l'on appelle dès sa création ((function(){...})()

Notre code devient maintenant :

 
Sélectionnez
(function(){
    var prefixer = new PrefixFinder(),
        tt = new Date(),
        jsTransform = prefixer.getPrefixedProp('transform').js,
        cssTransform = prefixer.getPrefixedProp('transform').css,
        jsAnim = prefixer.getPrefixedProp('animation').js,
        $sep = document.createElement('div'),
        $sepactu,
        $horloge = document.getElementById('horloge'),
        secondeCSS = document.getElementById('seconde').style,
        minuteCSS = document.getElementById('minute').style,
        heureCSS = document.getElementById('heure').style;
    var styleElement = document.createElement('style');
    document.getElementsByTagName('script')[0].parentNode.insertBefore(styleElement, document.getElementsByTagName('script')[0]);
    try{
        styleElement.sheet.insertRule('@keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
    }
    catch(e){
        try{
            styleElement.sheet.insertRule('@' + prefixer.prefixe.css + 'keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
        }
        catch(e){
            console.log('Pas d\'animation');
        }
    }
    $sep.className = 'sep';
    secondeCSS[jsAnim] = 'tour 60s -' + (tt.getSeconds()) + 's steps(60, end) infinite';
    secondeCSS[jsTransform + 'Origin'] = '50% 175px';
    minuteCSS[jsAnim] = 'tour 3600s -' + (tt.getMinutes()*60 + tt.getSeconds()) + 's steps(60, end) infinite';
    minuteCSS[jsTransform + 'Origin'] = '50% 145px';
    heureCSS[jsAnim] = 'tour 43200s -' + ((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's linear infinite';
    heureCSS[jsTransform + 'Origin'] = '50% 100px';
    for(var i = 0; i < 12; i++){
        $sepactu = $sep.cloneNode(true);
        $sepactu.style[jsTransform] = 'rotate(' + (i*30) + 'deg) translateY(-190px)';
        $horloge.appendChild($sepactu);
    }
})()

Cette technique permet de préserver le contexte global et au ramasse-miettes (garbage collector) de supprimer les variables devenues inutiles.

Notre code est quasiment complet. Mais nous n'avons traité que les navigateurs compatibles, ce qui représente tous les navigateurs courants sauf Internet Explorer versions 9 et inférieures.
Nous allons donc prendre en compte les navigateurs non compatibles. Nous allons gérer ce cas dans le second bloc catch, en effet, si le navigateur reconnaît une forme préfixée ou non de @keyframes, alors le reste du code sera supporté, sinon, inutile d'aller plus loin.

Voici notre code définitif (voir le résultat en ligne) :

 
Sélectionnez
(function(){
    var prefixer = new PrefixFinder(),
        tt = new Date(),
        jsTransform = prefixer.getPrefixedProp('transform').js,
        cssTransform = prefixer.getPrefixedProp('transform').css,
        jsAnim = prefixer.getPrefixedProp('animation').js,
        $sep = document.createElement('div'),
        $sepactu,
        $horloge = document.getElementById('horloge'),
        secondeCSS = document.getElementById('seconde').style,
        minuteCSS = document.getElementById('minute').style,
        heureCSS = document.getElementById('heure').style;
    var styleElement = document.createElement('style');
    document.getElementsByTagName('script')[0].parentNode.insertBefore(styleElement, document.getElementsByTagName('script')[0]);
    try{
        styleElement.sheet.insertRule('@keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
    }
    catch(e){
        try{
            styleElement.sheet.insertRule('@' + prefixer.prefixe.css + 'keyframes tour{from{' + cssTransform + ': rotate(0deg);}to{' + cssTransform + ': rotate(360deg)}}', 0);
        }
        catch(e){
            $horloge.id = '';
            $horloge.className = '';
            $horloge.style.border = '1px solid #25465B';
            $horloge.style.margin = '10px 40px';
            $horloge.style.padding = '10px 10px 10px 50px';
            $horloge.style.background = 'url("http://www.developpez.com/template/kit/kiterror.png") no-repeat scroll 10px center #FFBCC5';
            $horloge.innerHTML = 'Votre navigateur est trop ancien pour supporter cet exemple.<br />Vous devriez le mettre à jour pour pouvoir profiter pleinement des nouveautés offertes par HTML5 / CSS 3 / JavaScript.';
            return false;
        }
    }
    $sep.className = 'sep';
    secondeCSS[jsAnim] = 'tour 60s -' + (tt.getSeconds()) + 's steps(60, end) infinite';
    secondeCSS[jsTransform + 'Origin'] = '50% 175px';
    minuteCSS[jsAnim] = 'tour 3600s -' + (tt.getMinutes()*60 + tt.getSeconds()) + 's steps(60, end) infinite';
    minuteCSS[jsTransform + 'Origin'] = '50% 145px';
    heureCSS[jsAnim] = 'tour 43200s -' + ((tt.getHours()%12)*3600 + tt.getMinutes()*60 + tt.getSeconds()) + 's linear infinite';
    heureCSS[jsTransform + 'Origin'] = '50% 100px';
    for(var i = 0; i < 12; i++){
        $sepactu = $sep.cloneNode(true);
        $sepactu.style[jsTransform] = 'rotate(' + (i*30) + 'deg) translateY(-190px)';
        $horloge.appendChild($sepactu);
    }
})()

V. Conclusion et remerciements

Les nombreuses fonctionnalités apportées par HTML5 et CSS 3 permettent de réaliser des choses que l'on n'osait même pas imaginer il y a quelques années.
Si la syntaxe CSS semble simple, il est cependant nécessaire de bien comprendre le fonctionnement d'une propriété pour éviter d'obtenir des résultats inattendus.

Je tiens à remercier Torgar, NoSmoking et jreaux62 pour leur aide technique et graphique, ainsi que ClaudeLELOUP pour sa relecture attentive.

N'hésitez pas à faire part de vos remarques et commentaires sur le forum. 7 commentaires Donner une note à l'article (5)

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Didier Mouronval. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.