CSS & Trigonométrie

La trigonométrie débarque dans CSS3. Petit tuto pratique pour voir un cas d'usage de l'utilisation de ces nouvelles fonctions.

Auteur :
Jonathan Marco
Difficulté :
Défi protocolaire (intermédiaire)

Bienvenue mes cadettes et cadets ! Cette semaine, pour un projet, j'ai voulu créer cette horloge.

La difficulté était de faire en sorte que les chiffres soient bien répartis sur le pourtour du cercle. Dans un premier temps, je voulais partir sur du Javascript, car j'avais déjà fait un peu de trigonométrie pour un ancien projet avec ce langage, mais lors de ma veille documentaire, j'ai découvert une petite nouveauté qui est arrivée cette année dans CSS 3 : tout le matériel pour faire de la trigonométrie. La fonctionnalité a été proposée par Safari en mars 2022, puis a été validée par le W3C : 10.4. Mathematical expressions - Trigonometric functions, avant d'être implémentée de décembre 2022 à mars 2023 par les autres navigateurs.

Rappelez-vous ces douces années de lycée, avec les cours de trigonométrie qu'on a toutes et tous adorés !

Rappels de trigonométrie

Le cercle trigonométrique

Un cercle trigonométrique est une figure géométrique, ayant un rayon d'une unité (m, cm, dm, mm...). Dans ce cercle passent les axes d'un graphique : axe des abscisses (axe des x) et axe des ordonnées (axe des y).

Comme l'unité n'est pas définie, cela permet de toujours avoir, quel que soit l'usage qu'on a de ce cercle, une valeur de x ou y comprise entre 0 et 1. Si, par exemple, je travaille sur un cercle de rayon 360px, j'aurai une valeur comprise entre 0 et 360px.

Le cercle permet alors de calculer n'importe quel angle grâce aux fonctions : cosinus (cos) et sinus (sin). Je vois bien que celles et ceux du fond de la classe se demandent : "Mais pourquoi il nous parle de ce cercle trigonométrique pour placer les chiffres de l'horloge ?" Vous allez voir c'est simple !

Cosinus et sinus

L'avantage que nous apporte ce cercle, c'est qu'il nous permet de calculer des cosinus et des sinus simplement, et ce, sans calculatrice ! En effet, le cosinus est la distance du point rapportée sur l'axe des x et le sinus celle rapportée sur l'axe des y. Petit exemple :

Pour un angle α, la distance entre 0 et px (en orange ci-dessus) correspond à la valeur de cos(α), alors que la distance entre 0 et py (en bleu ci-dessus) correspond à sin(α). C'est cette propriété qui va nous intéresser pour placer nos chiffres.

Angle et trigonométrie

Dans la vie quotidienne, quand on parle d'angle, on a tendance à utiliser les degrés. En trigonométrie, on utilisera plus souvent les radians. L'angle en radians correspond à la longueur de l'arc de cercle formalisé par l'angle (partie verte sur l'image ci-dessus). Pour rappel, le périmètre d'un cercle se calcule avec la formule suivante : 2 * π * R où R est le rayon du cercle.

Pour s'en souvenir, un moyen mnémotechnique : quand on lit la formule rapidement = "2 pi R" ou "deux pierres".

Pour faire la conversion "degrés → radians", on va utiliser un cercle complet. Si l'on prend un disque entier, l'angle en degrés sera de 360°. En radian, on prend la longueur de l'arc de cercle, qui correspond au cercle entier, donc à son périmètre. Il sera donc de 2 * π * R, sachant qu'on a dit plus haut que notre cercle avait comme particularité d'avoir un rayon d'une unité, le résultat final en radians sera : 360° = 2π rad. On peut ensuite calculer les autres angles 180° = π rad, 90° = π/2 rad, ...

Deuxième chose à savoir, les angles en radians peuvent être "simplifiés" : 

  • Un cercle trigonométrique se lit dans le sens antihoraire. Un angle de 90° vers le haut du cercle sera de π/2 rad, là où le même angle vers le bas sera de 270° et donc de 3π/2 rad ou de -π/2 rad, ce qui équivaudrait à -90°, notation qu'on utilise peu en degrés.
  • Tout angle supérieur à 2π rad pourra être simplifié en lui retranchant "2π". Un angle de 450° équivaut à 5π/2 rad et sera donc simplifié en π/2 rad.

Construction de notre horloge

Le code de départ

Maintenant qu'on s'est bien rafraîchi la mémoire en ce qui concerne la trigonométrie, on a tout ce qu'il nous faut pour créer notre horloge. Pour commencer, je construis le HTML. Pour rappel, l'horloge doit ressembler à ça :

<!doctype html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Horloge</title>
</head>
<body>
<section>
  <div id="clock">
    <div></div>
    <div></div>

    <button>12</button>
    <button>24</button>

    <span>1</span>
    <span>2</span>
    <span>3</span>
    <span>4</span>
    <span>5</span>
    <span>6</span>
    <span>7</span>
    <span>8</span>
    <span>9</span>
    <span>10</span>
    <span>11</span>
    <span>12</span>
  </div>
</section>
</body>
</html>

Le code HTML est assez simple :

  • Une section pour englober notre horloge et la centrer facilement sur notre page. Elle n'est pas indispensable, elle ne me sert que pour avoir un environnement de travail propre pour le projet actuel.
  • La div avec l'id "clock", qui sera le conteneur des autres éléments HTML,. Elle portera le fond de notre horloge (le cercle un peu plus foncé).
  • Les deux premières div enfants de div#clock représentant, dans l'ordre, le petit rond au centre de l'horloge et l'aiguille.
  • Les deux button pour changer entre le matin et l'après-midi.
  • Et les douze span pour représenter les chiffres du cadran qu'on va devoir placer avec nos fonctions trigonométriques.

On met en place le CSS de départ :

:root {
    --page-bgc: rgb(25, 25, 25);
    --clock-bgc: rgb(18, 18, 18);

    --clock-size: 320px;
}

body {
    margin: 0;
}

section {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: var(--page-bgc);
    width: 100vw;
    height: 100vh;
}

#clock {
    width: var(--clock-size);
    height: var(--clock-size);
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    background-color: var(--clock-bgc);
    color: white;
}

Dans l'ordre :

  • Je définis les variables CSS dans :root. Pour plus de détails, vous référer à la partie "Variables CSS" de ce chapitre.
  • Dans body, je supprime la marge par défaut pour avoir une page complète.
  • Pour la section :
    • J'ajoute l'option flex pour centrer notre horloge sur l'axe principal et secondaire de notre page. Pour rappel, vous référer au cours sur le Flex.
    • Je mets une couleur de fond via la variable --page-bgc.
    • Je fais en sorte que la section prenne toute la largeur de la page (vw) et toute sa hauteur (vh).
  • Pour la div avec l'id "clock" :
    • J'utilise la variable --clock-size pour width et height pour qu'elle ait la même taille en horizontal et en vertical.
    • J'ajoute l'option flex pour centrer tous les éléments au centre de l'horloge. J'expliquerai plus bas pourquoi.
    • Je mets un border-radius à 50% pour avoir un cercle parfait.
    • J'ajoute une couleur de fond via la variable --clock-bgc.
    • Je mets la couleur du texte en blanc avec color.

J'obtiens alors un cercle plus foncé (fond de notre horloge) avec les deux boutons qui ont leur design par défaut et les douze chiffres écrits en blanc, le tout aligné au centre du cercle par flex.

Placer les boutons

À partir d'ici, pour vous entraîner, je vais donner des consignes de design afin de vous permettre de tester de votre côté, avant de vous fournir le code de correction.

On va commencer par la partie "simple" : les boutons "12" et "24" qui nous permettront de sélectionner le matin ou l'après-midi. Pour commencer, on va s'occuper de leur design. Ils ont trois états :

  • Par défaut = non sélectionné et sans survol : la police est blanche à 1rem, pas de bordure, pas de couleur de fond, les boutons font 36px de haut et de large et ils sont ronds.
  • Au survol : le fond est noir transparent (rgba(0, 0, 0, .6)), le curseur de la souris doit avoir une forme de main spéciale pour le clic.
  • Sélectionné : le fond est bleu (rgb(50, 243, 255)), la police est noire. Pour cet état, une class selected sera ajoutée en Javascript. Pour tester, ajoutez manuellement la classe au bouton "12" dans le HTML, en production la classe sera ajoutée au clic par Javascript.

Pour le placement, ils sont en position absolute (attention au placement en absolute, il a une petite spécificité qu'il ne faut pas oublier, si besoin vous pouvez revoir la partie dédiée sur ce cours) et les positions respectives sont :

  • Bouton "12" = 0 par rapport à la gauche et au haut du parent.
  • Bouton "24" = 0 par rapport à la droite et au haut du parent.
Je vous laisse faire ce CSS et vous donne le code de correction ci-dessous. Pour plus de challenge, essayez de ne pas ajouter de classe aux boutons pour utiliser des sélecteurs spécifiques qui vous permettront de sélectionner les deux d'abord, puis l'un, puis l'autre. Pour vous rafraîchir la mémoire, vous pouvez revoir les sélecteurs CSS dans ce cours.

Code de correction :

:root {
  --page-bgc: rgb(25, 25, 25);
  --clock-bgc: rgb(18, 18, 18);

  --clock-size: 320px;

  --button-selected-bgc: rgba(50, 243, 255);
}

#clock button {
    --button-size: 36px;

    color: white;
    font-size: 1rem;
    border: none;
    background-color: transparent;
    height: var(--button-size);
    width: var(--button-size);
    border-radius: 50%;
    position: absolute;
    top: 0;
}

#clock button:first-of-type {
    left: 0;
}

#clock button:last-of-type {
    right: 0;
}

#clock button:hover {
    background-color: rgba(0, 0, 0, 0.6);
    cursor: pointer;
}

#clock button.selected {
    background-color: var(--button-selected-bgc);
    color: black;
}
  • Le sélecteur #clock button me permet de sélectionner les deux boutons d'un coup. Pour rappel, il veut dire "je sélectionne tous les button contenus dans un élément qui a l'id 'clock'". À l'intérieur, j'ai ajouté les élements demandés dans les consignes. On remarque plusieurs choses :
    • J'ai réglé la bordure sur none et le background-color sur transparent pour supprimer les valeurs par défaut liées au design des boutons.
    • Pour le width et le height, comme je veux un rond parfait, ils doivent être identiques. J'aurais pu mettre directement les valeurs en dur, mais je suis passé par une variable directement définie dans mon bouton. Cela permet d'avoir une seule valeur utilisée deux fois, ce qui fait que si demain je souhaite que mon bouton ait un diamètre de 40px, je ne change que la valeur de la variable et ça changera directement le width et le height en une seule modification.
    • Le position est en absolute comme indiqué dans les consignes. Si vous souhaitez que les boutons se positionnent correctement en fonction de #clock et non de la page générale, il faut ajouter position: relative à #clock. En effet, si vous avez relu le cours que je vous ai mis en lien plus haut, la position en absolu se fera en fonction du premier parent qui déclare une position autre que static.
    • Les deux sont alignés en haut de leur parent donc on met dans le sélecteur commun le top: 0.
  • Ensuite, je sélectionne le premier bouton à part pour l'aligner à gauche. Pour ça, j'utilise le sélecteur spécifique :first-of-type qui permet de sélectionner le premier élément de ce type (ici button) dans le parent. Je lui mets la valeur left: 0 pour aligner le bouton à gauche du parent.
  • Puis je fais de même pour le second bouton avec le sélecteur :last-of-type et la valeur right: 0 pour l'aligner à droite du parent.
  • Pour l'état "au survol" qui correspond au sélecteur :hover, j'ajoute la couleur de fond et le curseur demandé.
  • Pour l'état "selected", j'ai utilisé la sélection button.selected avec les deux sélecteurs collés pour dire "je veux sélectionner tous les button qui ont la classe selected". Puis j'ajoute la couleur de fond via la variable --button-selected-bgc et la couleur de police.

Vous devriez arriver au résultat demandé : le fond de l'horloge en noir, les boutons pour sélectionner le matin ou le soir de part et d'autre de l'horloge en haut.

Les chiffres de l'horloge

Maintenant que nos boutons sont parfaits, on va pouvoir passer au but de ce chapitre : placer les chiffres de notre horloge. Avant de commencer à utiliser la trigonométrie, on va commencer par faire en sorte :

  • que nos chiffres soient tous vraiment placés au centre de l'horloge = au niveau de l'origine sur notre graphique, les uns au dessus des autres ;
  • que les span soient rondes et fassent 30px de diamètre ;
  • qu'il y ait le même effet au survol que nos boutons "12" et "24".
Je vous laisse réfléchir au code pour ces consignes et je vous donne le corrigé ci-dessous.
#clock button:hover, #clock span:hover {
    background-color: rgba(0, 0, 0, 0.6);
    cursor: pointer;
}

#clock span {
    --number-size: 30px;

    position: absolute;
    width: var(--number-size);
    height: var(--number-size);
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
}

Vous pouvez voir que :

  • j'ai ajouté #clock span:hover au sélecteur #clock button:hover car j'ai bien dit que je voulais le même effet. Autant mutualiser le code !
  • pour les span :
    • j'ai utilisé la même technique que ci-dessus pour le width et le height avec la variable et j'ai mis un border-radius pour qu'elles soient rondes.
    • je les ai mises en absolute pour qu'elles se placent toutes en fonction de l'origine = centre du cercle. Le fait qu'elles soient au centre est lié au fait que #clock est en flex avec les options align-items et justify-contents à center.
    • j'ai également mis ces éléments en flex avec les mêmes options pour centrer le texte à l'intérieur.

Les chiffres et la trigonométrie

On y est ! On va utiliser nos fonctions cos et sin pour placer nos chiffres ! D'abord, petite analyse d'une horloge. Les chiffres sont tous répartis équitablement sur le périmètre de celle-ci. Il y a 12 chiffres, je rappelle qu'un cercle complet a un angle de 2π qui sera divisé en 12 parties, donc chacun des chiffres sera séparé des autres par un angle de 2π / 12 = π/6 rad. C'est le chiffre "3" qui est sur l'axe des abscisses et donc qui est à 0°, les autres se placeront en fonction de ça.

Petite particularité en informatique ! En CSS, comme en JS, l'axe des ordonnées n'est pas orienté vers le haut comme on a l'habitude de le voir sur les graphiques, il est orienté vers le bas. Donc quand on déplace un élément sur l'axe des y de 100px, il se déplacera vers le bas, si on le déplace de -100px il ira vers le haut. C'est pourquoi les angles en radians sont aussi inversés ! Si l'on a un angle positif, c'est un angle qui ira vers le bas = sens horaire et si c'est un angle négatif, il fera une rotation vers le haut = sens antihoraire.

J'ajoute une variable (--number-position) dans #clock span pour avoir la distance où placer les chiffres :

#clock span {
    --number-size: 30px;
    --number-position: calc(var(--clock-size) / 2 - var(--number-size));

    position: absolute;
    width: var(--number-size);
    height: var(--number-size);
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
}

Cette distance, c'est le résultat de calc  qui est une fonction CSS pour calculer une valeur en fonction d'autres valeurs. Pour la calculer, nous aurons besoin du rayon du cercle. Sachant que --clock-size correspond au width du cercle, cela correspond donc à son diamètre. Ainsi, pour avoir le rayon, on fait var(--clock-size) / 2, auquel j'enlève la taille de nos span pour qu'elles soient bien à l'intérieur.

Vous avez maintenant tout ce qu'il vous faut pour faire le placement des chiffres. Attention, plusieurs solutions sont possibles, veillez à l'optimisation de votre code !

Et voici la correction pour les fonctions trigonométriques :

#clock span {
    --number-size: 30px;
    --number-position: calc(var(--clock-size) / 2 - var(--number-size));
    --number-angle: 0;

    position: absolute;
    width: var(--number-size);
    height: var(--number-size);
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    transform: translate(calc(cos(var(--number-angle)) * var(--number-position)), calc(sin(var(--number-angle)) * var(--number-position)));
}

#clock span:nth-of-type(1) {
    --number-angle: calc(pi / -3);
}

#clock span:nth-of-type(2) {
    --number-angle: calc(pi / -6);
}

#clock span:nth-of-type(3) {
    --number-angle: 0;
}

#clock span:nth-of-type(4) {
    --number-angle: calc(pi / 6);
}

#clock span:nth-of-type(5) {
    --number-angle: calc(pi / 3);
}

#clock span:nth-of-type(6) {
    --number-angle: calc(pi / 2);
}

#clock span:nth-of-type(7) {
    --number-angle: calc(2 * pi / 3);
}

#clock span:nth-of-type(8) {
    --number-angle: calc(5 * pi / 6);
}

#clock span:nth-of-type(9) {
    --number-angle: pi;
}

#clock span:nth-of-type(10) {
    --number-angle: calc(-5 * pi / 6);
}

#clock span:nth-of-type(11) {
    --number-angle: calc(-2 * pi / 3);
}

#clock span:nth-of-type(12) {
    --number-angle: calc(pi / -2);
}

La formule la plus importante est celle-ci : 

transform: translate(calc(cos(var(--number-angle)) * var(--number-position)), calc(sin(var(--number-angle)) * var(--number-position)));

Pour rappel : 

  • translate prend la distance en px sur l'axe des x en premier paramètre, puis la valeur sur l'axe des y ; il peut aussi prendre en troisième paramètre la valeur sur l'axe des z quand on fait de la 3D.
  • cos calcule la distance de l'angle sur l'axe des x et le sin sur l'axe des y.

Si l'on décompose ce qu'on a :

  • J'ai ajouté une variable --number-angle avec une valeur de 0 par défaut pour l'intégrer ensuite dans nos fonctions trigonométriques.
  • Je place la variable ci-dessus dans cos en premier paramètre de translate car ça va nous donner la valeur de déplacement en x, puis dans sin en second paramètre pour avoir le déplacement en y. Comme on l'a dit plus haut, cette valeur sera toujours comprise entre 0 et 1 puisque le cercle trigonométrique a un rayon de 1 unité.
  • Je multiplie les valeurs retournées par les fonctions trigonométriques par la distance que je souhaite et qui correspond à la variable que je vous avais donnée avant le corrigé = --number-position pour avoir des valeurs comprises entre 0 et 130px.
  • Je place cette multiplication dans un calc pour avoir le produit. Le produit avec cos en premier paramètre et le produit avec sin en second.
Comme vous l'avez remarqué, je n'ai mis le transform qu'une seule fois car la valeur des angles, je ne l'ai pas mise en dur mais en variable. En utilisant cette technique, la formule complexe n'est écrite qu'une seule fois, ensuite ce n'est que la valeur de l'angle qui est modifiée pour chaque chiffre. On aurait pu mettre cette longue formule sous le sélecteur de chaque span spécifique, mais si un jour, la formule était légèrement modifiée, il faudrait le faire 12 fois ! Ici, elle ne serait modifiée qu'une seule fois.

Pour sélectionner les chiffres un par un, comme pour les boutons plus haut, j'ai utilisé le pseudo-sélecteur "nth-of-type". Puis j'ai modifié les angles dans chaque sélection : 

  • 1 : angle en degré = 60° vers le haut par rapport à l'axe des x, ce qui équivaut à -π/3. Attention, je vous ai dit, en préambule de cette partie, que le sens des angles était inversé en CSS, donc dans le sens antihoraire, ils sont négatifs.
  • 2 : -30° = -π/6.
  • 3 : 0
  • 4 : 30° = π/6.
  • 5 : 60° = π/3.
  • 6 : 90° = π/2.
  • 7 : 120° = 2π/3.
  • 8 : 150° = 5π/6.
  • 9 : 180° = π.
  • 10 : à partir de là on peut simplifier en partant dans le sens antihoraire, 210° vers le bas ou -150° vers le haut, donc -5π/6.
  • 11 : 240° ou -120° = -2π/3.
  • 12 : 270° ou -90° = -π/2.

Et voilà, on a créé notre horloge ! Pour ce qui est de son fonctionnement, on utilisera du Javascript, mais ça, c'est une autre histoire !

L'aiguille

Comme pour les boutons "12" et "24", pour passer les chiffres en mode "selected", on aura besoin de Javascript. Pour l'heure, on peut simuler que le 12 de notre horloge est sélectionné, placer le rond autour de ce chiffre et créer l'aiguille ainsi le centre de notre horloge. Voici les consignes :

  • Le rond central sera de la même couleur que le bleu des boutons "12" et "24" en mode "selected". Il sera rond avec un diamètre de 10px et centré par rapport à l'horloge.
  • L'aiguille ira du centre du rond central au centre du chiffre sélectionné. Elle aura une largeur de 2px et sera de la même couleur que ci-dessus.
  • Le chiffre "12" aura une classe "selected" qui lui donnera la même couleur et fera passer la police en noir.
Dernier exercice, c'est à vous de jouer !

Dernière correction :

#clock div {
    position: absolute;
    background-color: var(--button-selected-bgc);
}

#clock div:nth-child(1) {
    width: 10px;
    height: 10px;
    border-radius: 50%;
}

#clock div:nth-child(2) {
    --hand-length: calc(var(--clock-radius) - var(--number-size) - 10px);
    width: 2px;
    height: var(--hand-length);
    top: calc(var(--clock-radius) - var(--hand-length));
}

Pour le détail :

  • Dans le premier bloc, je mets les modifications communes aux deux div : la couleur et le fait que celles-ci soient en absolute.
  • Pour la partie centrale, la première div, je lui mets la même taille en largeur et en hauteur, ainsi que le border-radius pour qu'elle soit ronde.
  • Pour l'aiguille, la seconde div, j'en règle la largeur à 2px.
    • Ensuite, pour la longueur, comme dit dans l'énoncé, je veux que ce soit la longueur entre le cercle central et le chiffre, c'est donc le rayon de notre cercle moins la taille de notre chiffre ; je mets donc les deux variables dans un calc et j'enlève en plus 10px, ce qui correspond à la marge qu'on a mise autour des chiffres.
    • Enfin, pour le top, définissant le placement, je mets le rayon du cercle moins la taille de l'aiguille, ce qui permet de ne garder que l'espace au-dessus de l'aiguille.

Cliquez ici pour télécharger le code complet.

  • Cours
  • Tutoriels
  • Aide
  • À Propos