Saltar al contenido principal

Cómo crear un reloj analógico con Javascript

Qué vamos a hacer

Seguimos con la serie de artículos en las que vamos a construir pequeños proyectos como excusa para aprender programación. En este segundo episodio vamos a seguir con el día 2 de #Javascript30, el reto de construir 30 pequeñas aplicaciones web con Javascript.

Este tiene un enunciado sencillo, pero no te dejes engañar, si no sabes cómo abordarlo puede que al principio te cueste. Para este reto se pide crear un reloj analógico con CSS y JS.

Este artículo pertenece a la serie de artículos con contenido práctico llamada Sortilegios 🚧

🗺️ Hoja de ruta

Manos a la obra 👷

Creado la vista y los estilos

Lo que voy a hacer es hacerlo con SVG. Aunque también se puede hacer con elementos HTML he preferido hacerlo así para dar alguna pincelada sobre SVG.

Empecemos por el círculo:

<div class="clock">
  <svg
    class="circle"
    viewBox="0 0 120 120"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
  >
    <circle cx="60" cy="60" r="60" />
  </svg>
</div>

Creamos un div simplemente para meter el SVG que contendrá la esfera del reloj y las manecillas.

Como ves, en SVG existe un elemento destinado a crear círculos. Las propiedades cx y cy marcan la posición del círculo dentro del reloj, en esta caso al ser la mitad que la viewBox, se situará en el centro.

De momento simplemente voy a posicionar el reloj en el centro usando CSS.

.clock {
  margin: 0px auto;
  width: 650px;
  height: 650px;
}

Como no hemos definido un color para el círculo por defecto se pintará en negro. Para añadir un color tienes que hacerlo mediante la propiedad fill, la puedes añadir en el HTML o en el CSS:

<circle cx="60" cy="60" r="60" fill="#fabada" />
circle {
  fill: #fabada;
}

Creando las manecillas

Vamos ahora con las manecillas. La idea es crear 3 líneas dentro del SVG (una para cada manecilla). En principio las voy a crear para que salgan desde la parte de arriba y vayan hasta el centro del reloj.

Las líneas serían así:

<line x1="60" y1="0" x2="60" y2="60" class="hours" />
<line x1="60" y1="0" x2="60" y2="60" class="minutes" />
<line x1="60" y1="0" x2="60" y2="60" class="seconds" />

Los parámetros x1 y y1 sirven para indicar la posición dentro del SVG del punto de partida. En este caso el punto de partida es 60 (el radio del círculo) para el eje X y 0 en el Y para que se coloque arriba en el centro del reloj.

Los parámetros x2 y y2 son para el punto de destino de la línea, en nuestro caso el centro del reloj, por lo tanto 60 y 60.

También he añadido clases CSS para poder cambiar el color de las líneas usando la propiedad stroke. Esto no lo hago con el fill porque con stroke quiero que sea como una propiedad de un borde, así puedo añadir stroke-linecap para redondearlo y que no sea una línea rectangular.

Además, para las 3 líneas he puesto que el transform-origin esté en el centro para que al rotar la línea se haga desde el centro del reloj.

.hours,
.minutes,
.seconds {
  transform-origin: center;
  stroke-linecap: round;
  stroke-width: 3px;
}
.hours {
  stroke: cyan;
}
.minutes {
  stroke: lime;
}
.seconds {
  stroke: fuchsia;
}

Vamos ahora con el Javascript.

Calculando la rotación de las manecillas

El Javascript parece que puede ser muy complicado pero si lo piensas no lo es tanto.

Simplemente lo que necesitamos es calcular los grados de rotación de cada manecilla pasando la hora actual. Haz una prueba, en el CSS pon esto:

.minutes {
  stroke: lime;
  transform: rotate(90deg);
}

¿Ves que la manecilla de los minutos ahora apunta hacia la derecha? Eso es gracias al transform-origin, tan solo tenemos que sacar los grados entre 0 y 360 (360 porque una circunferencia tiene 360 grados). Ya puedes quitar lo del transform.

Lo primero que he hecho es crear una función que se autoejecuta para que se lance cuando se cargue el Javascript.

(function () {
  calculateHourDegrees();
  calculateMinuteDegrees();
  calculateSeconds();
})();

Simplemente llamo a 3 funciones que voy a crear ahora para calcular los grados de cada manecilla.

Lo siguiente que hago es crear una función que servirá para hacer una especie de regla de 3 para poder calcular los grados:

function linearMap(value, min, max, newMin, newMax) {
  return newMin + ((newMax - newMin) * (value - min)) / (max - min);
}

Es muy simple, pasas un número value y con min y max pones el rango que tiene ese valor, es decir, el mínimo valor y el máximo que puede tener ese número. Por último, pasas el nuevo valor mínimo con newMiny el nuevo máximo con newMax y la función te devolverá el nuevo valor en el nuevo rango.

Pongamos un ejemplo. Imagina que queremos calcular los grados (entre 0 grados y 360 grados como hemos dicho) de la manecilla de los minutos. Pongamos que son las 12:33, la llamada a esa función sería así:

linearMap(33, 0, 60, 0, 360);

El primer parámetro son los minutos, 33, min y max son 0 y 60 porque los minutos como mucho pueden ser 60 y el nuevo valor mínimo y el máximo es 0 y 360. En otras palabras, es una simple regla de 3 que uso para sacar los grados.

Sabiendo esto ya podemos crear la función para calcular la manecilla de las horas:

function calculateHourDegrees() {
  const currentHour = new Date().getHours() - 12;
  const angle = linearMap(currentHour, 0, 12, 0, 360);
  document.querySelector(".hours").style.transform = `rotate(${angle}deg)`;
}

Lo primero que hago es sacar la hora actual y le resto 12 (porque puede ir hasta 24 pero en un reloj analógico nos vale con 12).

Saco el ángulo con la función que he explicado antes y lo que hago es seleccionar con el querySelector el elemento del HTML con la línea de las horas para ponerle como estilo el transform con los grados.

Las otras funciones, la de los minutos y segundos es igual:

function calculateMinuteDegrees() {
  const currentMinutes = new Date().getMinutes();
  const angle = linearMap(currentMinutes, 0, 60, 0, 360);
  document.querySelector(".minutes").style.transform = `rotate(${angle}deg)`;
}

function calculateSeconds() {
  const currentMinutes = new Date().getSeconds();
  const angle = linearMap(currentMinutes, 0, 60, 0, 360);
  document.querySelector(".seconds").style.transform = `rotate(${angle}deg)`;
}

Con eso ya se calculan los grados del reloj cuando cargamos el HTML y el Javascript, pero falta algo, ir actualizando el reloj según cambia la hora. Para ello voy a envolver el calculo de los grados dentro de un setInterval:

(function () {
  setInterval(() => {
    calculateHourDegrees();
    calculateMinuteDegrees();
    calculateSeconds();
  }, 1000);
})();

El setInterval lo que va a hacer es llamar a las 3 funciones cada segundo (por eso pone 1000 porque son 1000 milisegundos). El reloj quedaría así:

En la imagen se aprecia un círculo negro con 3 manecillas

Por cierto, en el HTML, en cada línea, he ajustado el valor y1 para que cada manecilla mida distinto, como en los relojes analógicos. Esto va al gusto de cada uno.

Por último para dejarlo fino fino voy a meter dentro del CSS una transición de la propiedad transform de las manecillas de las horas y de los minutos para que cuando cambie el valor del ángulo el cambio no sea brusco.

.hours,
.minutes,
.seconds {
  transform-origin: center;
  stroke-linecap: round;
}

.hours {
  stroke: fuchsia;
  stroke-width: 3px;
  transition: transform 1s ease-in-out;
}
.minutes {
  stroke-width: 2px;
  stroke: lime;
  transition: transform 1s ease-in-out;
}
.seconds {
  stroke: white;
}

Pero esto no acaba aquí, como detalle final voy a pintar las típicas líneas para marcar las horas alrededor de las manecillas.

Ahora lo que voy a hacer es crear 12 líneas (una para señalar cada hora) dentro del SVG:

<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />
<line x1="60" y1="5" x2="60" y2="10" class="line" />

He ajustado el x e y de cada punto para que midan poquito y se dibujen cerca del borde. Ahora, como pasa con las manecillas hay que calcular los grados de cada una de las líneas para que se dibujen alrededor del círculo. Lo primero el CSS, parecido a las manecillas:

.line {
  stroke-width: 1px;
  stroke: white;
  stroke-linecap: round;
  transform-origin: center;
}

Y por último el Javascript. Antes de seguir leyendo piensa primero cómo lo harías e intenta hacerlo sin mirar cómo lo he hecho yo.

Yo lo que se me ha ocurrido es tener en una lista de objetos todas las líneas para recorrerla con bucle for e ir poniendo los ángulos a cada línea.

function calculateLines() {
  const lines = document.querySelectorAll(".line");
  const numberLines = lines.length;
  for (let i = 0; i < numberLines; i++) {
    const line = lines[i];
    const angle = linearMap(i, 0, numberLines, 0, 360);
    line.style.transform = `rotate(${angle}deg)`;
  }
}

Con querySelectorAll pillo en forma de lista todos los elementos del HTML con la clase "line". Usando un bucle for recorro las líneas y con la función de linearMap que hemos creado antes calculo los grados para cada línea. Fíjate en el detalle que ahora el primer parámetro que paso es i, es decir, el número de esa línea, que va desde 0 al número de líneas que haya, así se reparten las líneas entre la circunferencia y se quedan a la misma distancia.

Lo bueno de hacerlo así es que si ahora decides que quieres más o menos líneas alrededor de la esfera simplemente tienes que añadir o quitar las líneas del HTML. Automáticamente se calculará su posición para que queden repartidas en el reloj.

Resultado final:

En la imagen se aprecia un círculo negro con 3 manecillas y líneas alrededor para cada 5 minutos

Demo y código fuente

https://codepen.io/Frostq/pen/XQNXpq

Deberes

Si quieres seguir practicando cosas con el reloj te propongo este par de ejercicios:

Conclusiones

Este tipo de proyectos te recomiendo que primero los intentes hacer sin mirar el código que aparece aquí (puedes leer más o menos como lo planteo por encima) porque cuando lo haces por ti mismo y te sale sin mirar la solución te sientes muy bien.

Si lo has hecho mirando y copiando mi código no pasa nada, al principio este tipo de ejercicios suele costar, pero te recomiendo que vuelvas a intentar hacerlo sin mirar dentro de un tiempo.

También te digo que el código que yo explico puede que no sea el mejor o el más óptimo, simplemente es mi solución, seguramente haya otras mucho mejores.