soy Kseso y esto EsCSS

Relojes analógicos y digitales single element, 1 motor CSS y 12 líneas de JS

La realización de relojes funcionales, ya sean digitales o analógicos, son llevados por Furoya en este artículo a un nuevo nivel por su sencillez y mínimo código Js y CSS necesarios.

Relojes analógicos y digitales single element, 1 motor CSS y 12 líneas de JS

✎ 0
COLABORACIÓN AUTOR INVITADO
Relojes analógicos y digitales con motor CSS, pero con arranque JS

Título original del Autor: Relojes analógicos y digitales con motor CSS, pero con arranque JS.

Siempre insisto en que éste es un blog sobre CSS, y que los aportes que hagan HTML o JS son un complemento para donde los estilos anabolizados que tienen las páginas web (y las aplicaciones web) no llegan. Y aún así, esta vez vamos a terminar el artículo con un ejemplo lleno de métodos de javascript, casi sin formato ... para que se lo agreguen los lectores a su gusto en sus propias versiones ;-)

Pero igual, para compensar, tenemos un comienzo de abuso de CSS.

El reloj que presentamos inicialmente sería uno más de los cientos que hay publicados y hasta explicados en la red; excepto por algunas dificultades técnicas que nos vamos a imponer, y que servirán para practicar soluciones en casos reales el día de mañana:

Porque esta versión del reloj no sólo es analógica, sino que:

  1. es single-element: tiene un único motor para ambas agujas.
  2. el motor además es puro CSS: 3 reglas y 1 sóla animación para ambas agujas.
  3. y sólo 17 líneas de javascript: empezamos a practicar con código que está en borrador para el W3C pero ya parece que queda así, y al menos Blink y Mozilla lo reconocen. Microsoft lo hará algún día.

Después seguimos con una versión más compatible, pero todavía con algunos trucos para aprovechar el ejemplo. Y mantenemos un single-motor, como corresponde a cualquier reloj medianamente decente y reducimos a 12 las líneas de javascript necesarias.

)

El último ya es sólo para estudiar. Este engendro mezcla diferentes modos de hacer relojes digitales en una misma demo, y es quizá al que le vamos a dedicar más tiempo (valga la redundancia).

Además, los invito a darse una vuelta por versiones anteriores ya publicadas aquí, donde aparecen un viejo reloj Casio o un cronómetro digital de donde pueden sacar varias ideas.

Un elemento, un motor, un reloj.

Antes de ver la demo, les sugiero revisar con cuidado el CSS. Es cierto que nuestra comunidad es de profesionales o desarrolladores avanzados (ya tenemos asumido que no es un sitio para principiantes, pero sí para estudiosos) y que algunas propiedades no van a ser novedad, pero seguramente la mayoría no tuvo aún muchas oportunidades de utilizarlas. El creador y responsable de este blog ya publicó varios artículos sobre las Propiedades de Autor o Custom Properties de CSS:

Y en este artículo les vamos a hacer honor en el primer ejemplo:

See the Pen Analog clock single element, single CSS engine. by Kseso (@Kseso) on CodePen.

Nota del editor: En el pen original Furoya sólo emplea las custom properties de CSS en la regla @keyframes gira {} y es a ellas a las que se refiere. Ha sido cosa del Editor emplearlas también en el gradiente del fondo del reloj para facilitar su modificación estética.

El CSS tiene demasiado adorno, pero es porque toda la carátula y ambas agujas están creadas con estilos dentro de un div. No hay más HTML.

No voy a explicar todo el formato, pero sí me detengo un poco en dos reglas que son importantes para la animación:

  1. El uso de will-change: contents* no se ve muy seguido, pero en navegadores blink se hace indispensable porque no maneja los mismos tiempos de (por ejemplo) mozilla para el rendering final cuando se hacen cambios dinámicos, y no hablo solamente de los hechos por javascript, sino también de las mismas animaciones CSS. En este ejemplo dejé "comentadas" unas lineas JS con un viejo truco para cuando no existía el will-change, que forzaba al navegador a modificar el entorno del elemento, preparándolo para cambios de formato. Por si les interesa, esto pasaba también en Internet Explorer 5.5, por eso los viejos como yo ya sabemos cómo enfrentarnos al problema. Que antes parchábamos también con un simple zoom:1 en la hoja se estilos sin recurrir al javascript, pero ahora por fin lo resolvemos desde CSS legalmente. (Más información en will-change Propiedad css para anticipar y preparar los cambios.)
  2. Se usa el valor contents porque justamente lo que va a cambiar es el par de pseudos que CSS va a crear dentro del div, con una transformación disparada (tardíamente) desde un CSS escrito con javascript al final de la última hoja de estilos. Es que los keyframes con la transformación para los pseudoelementos contienen dos valores en "pseudovariables" que no aparecen en el código fuente, y las calcula el JS según la hora de la máquina antes de escribirlas al vuelo, después de cargar todo el documento.
  3. *: Al hablar de la propiedad will-change es obligado reseñar la advertencia que el consorcio hace en su documentación "Using 'will-change' Well" por los peligros potenciales que un abuso puede suponer para el rendimiento del navegador.

Veamos el escript

/* FUNCIÓN QUE CAPTURA LA HORA LOCAL Y LA PASA AL RELOJ CSS */ function tiempo() { //document.body.style.zoom = "1.0000001"; /* PONE HORA Y MINUTO EN VARIABLES */ var fecha = new Date(); var hora = fecha.getHours(); var minuto = fecha.getMinutes(); /* REDUCE EL FORMATO A 12 HORAS */ hora = (hora>11)? hora-12 : hora; /* CALCULA GRADOS PARA LA POSICIÓN INICIAL DE AGUJA HORARIA */ var gradosHoraI = ((hora * 60) + minuto) / 2; /* CALCULA GRADOS PARA LA POSICIÓN INICIAL DE AGUJA MINUTERA */ var gradosMinutoI = minuto * 6; /* CALCULA GIRO COMPLETO PARA LA POSICIÓN FINAL DE AGUJA HORARIA */ var gradosHoraF = gradosHoraI + 360; /* CALCULA GIRO COMPLETO PARA LA POSICIÓN FINAL DE AGUJA MINUTERA */ var gradosMinutoF = gradosMinutoI + 360; /* CUENTA EL ÍNDICE DE LA ÚLTIMA HOJA DE ESTILOS EN EL DOCUMENTO */ var totalEstilos = document.styleSheets.length-1; /* INSERTA AL FINAL DE ÚLTIMA HOJA DE ESTILOS LAS REGLAS PARA ::before */ document.styleSheets[totalEstilos].insertRule("#reloj::before{" +"--tiempoI: rotate("+ gradosMinutoI +"deg); --tiempoF: rotate("+ gradosMinutoF +"deg)}", document.styleSheets[totalEstilos].cssRules.length); /* INSERTA AL FINAL DE LA NUEVA ÚLTIMA HOJA DE ESTILOS LAS REGLAS PARA ::after */ document.styleSheets[totalEstilos].insertRule("#reloj::after{" +"--tiempoI: rotate("+ gradosHoraI +"deg); --tiempoF: rotate("+ gradosHoraF +"deg)}", document.styleSheets[totalEstilos].cssRules.length); } /* EJECUTA LA FUNCIÓN DESPUÉS DE CARGAR TODOS LOS ARCHIVOS */ window.addEventListener("load", tiempo, false); //document.addEventListener("DOMContentLoaded", tiempo, false);

Hay mucha cuenta que no voy a detallar, pero verán que el código es bastante sencillo.

  • Empezamos con la función que lee hora y minuto al abrir la página, convierte esos valores a ángulos que coinciden con los que tendría cada aguja de reloj analógico real y las pasa a los pseudo elementos.
  • En la primera linea dejé el método que les mencioné antes, poniendo un zoom lo bastante chico como para ser imperceptible ya forzamos al navegador a crear una capa lista para recibir cambios y animaciones más propias de filtros que de estilos. Está desactivado porque más abajo lo resolvimos con CSS, y queda como una curiosidad. Que no es la única manera; es la única que publico, nomás.
  • Después capturamos hora y minuto, que están en formato de 24 horas, y como los relojes analógicos tienen formato de 12, hay que hacer la corrección restando 12 después de las 11:59.
  • El problema más importante que enfrenta el proyecto es la posición inicial de las agujas. Una vez ubicadas el movimiento es simple, el motor CSS las hace girar 360°, en 1 hora a la minutera y en 12 horas a la horaria. Pero por las características mecánicas de los relojes, si —por ejemplo— son las 3:30, la aguja minutera va a empezar a 180°, pero la horaria no va a estar a los 90°, sino a mitad de camino entre las 3 y las 4; a los 105°. Calcular el ángulo por minuto es fácil: 360° son 60 minutos, así que cada minuto son 6°. Multiplicamos el minuto que nos devuelve la máquina por 6 y ése es el total de grados iniciales para rotar la aguja. El ángulo por hora es apenas más complicado: primero hay que convertir todo el tiempo a minutos, es decir, multiplicar la hora por 60 y sumarle los minutos que devolvió el clock de la máquina; y luego los dividimos por 2 para que dé el ángulo inicial de la aguja horaria.
  • Los ángulos finales son obvios, como cada aguja tiene que dar una vuelta completa antes de empezar de nuevo, con sumarle 360 al ángulo inicial, ya está resuelto.
  • Ahora hay que pasar esos grados a los pseudoelementos que forman las agujas, y lo primero que hacemos es contar las hojas de estilo para identificar la última, y escribir en ella los valores.
  • El mecanismo se ve complejo, pero en realidad es muy simple. En el documento buscamos la colección de hojas de estilo, y como así tenemos el índice de la última, lo usamos para ubicarla e insertar las reglas para ::before. En el primer argumento de insertRule(regla,índice) escribimos toda la regla tal como estaría en los estilos, y lo que en realidad ponemos allí son las custom properties que vamos a pasar a las keyframes para que roten la aguja de minutos, con los valores calculados previamente. Una vez escritos, la animación empezará a funcionar. En el segundo argumento va el índice de la regla dentro de la hoja elegida, que debe ser mayor a la última que ya tenga escrita. Como tambien podemos contar el total de esa colección, y ya sabemos que el total siempre es un número mayor al índice del último de sus elementos, con poner su length es suficiente.
  • Luego hacemos lo mismo para ::after, que es la aguja horaria, pero con sus correspondientes valores.
  • Por último, agregamos un escuchador del evento de carga (de todos los archivos) para el objeto window, que es quien lo reconoce (no todos los objetos aceptan load). Podríamos usar DOMContentLoaded para el documento, pero la función se ejecutaría al cargarse el código fuente, no va a esperar a cargar las hojas de estilo linkeadas que son las que necesitamos trabajar. De cualquier forma, se los dejo comentado para que hagan sus pruebas.

Con imágenes, y más compatible.

Este segundo reloj es un poco más vistoso que el primero y usa los mismos mecanismos, pero ya sin "custom properties" ni "single element", porque si agregamos imágenes entonces tenemos más de un elemento de movida. Esto simplifica mucho el JS además del CSS que termina siendo más compatible.

Elegí una ilustración de IABI para trabajar, pero cada uno se hace la suya a gusto y medida.

See the Pen Analog clock with images by Kseso (@Kseso) on CodePen.

En el escript vemos las sutiles diferencias para comparar con el anterior.

function tiempo() { var fecha = new Date(); var hora = fecha.getHours(); var minuto = fecha.getMinutes(); hora = (hora>11)? hora-12 : hora; var gradosHora = ((hora * 60) + minuto) / 2; var gradosMinuto = minuto * 6; /* REFERENCIA AL SPAN QUE CONTIENE LA AGUJA HORARIA */ var aHoraria = document.getElementById("horaria"); /* REFERENCIA AL SPAN QUE CONTIENE LA AGUJA MINUTERA */ var aMinutera = document.getElementById("minutera"); /* ROTA LOS CONTENEDORES DE AGUJA A POSICIÓN INICIAL */ minutera.style.transform = "rotate("+ gradosMinuto +"deg)"; horaria.style.transform = "rotate("+ gradosHora +"deg)"; } window.addEventListener("load", tiempo, false);

En el HTML vemos que las agujas son imágenes del mismo tamaño, aunque el dibujo de la horaria es un poco más corto, y que están en sendos contenedores span ubicados para apuntar a los 0° o a las 12 en punto. La carátula está como fondo del contenedor general. Con estos datos, podemos entender claramente el funcionamiento del escript.

  • Capturamos el tiempo como de costumbre y extraemos hora y minuto. Hacemos el ajuste para 12 horas y convertimos a grados.
  • Ponemos en variables la referencia a cada contenedor de aguja. Solamente por prolijidad, porque nos podemos ahorrar el paso y escribirlos directamente en las siguientes líneas.
  • Estas últimas líneas de la función son todo lo que necesitamos para iniciar el reloj. Le pasan los ángulos a los contenedores para que se ubiquen según el tiempo de inicio, y luego CSS va a rotar 360° las imágenes que contienen. De esta forma, ya no hay que calcular el ángulo final ni pasar valores a pseudoelementos a través de las hojas de estilo.
  • El evento sigue siendo load, aunque ahora ya ni tiene importancia ejecutar la función antes o después que se carguen los archivos linkeados.

Y pensar que hasta hace pocos años, hacer un reloj analógico en una página web era toda una odisea.

Rejunte de métodos para reloj con calendario.

Vamos a ver un reloj digital. Está practicamente sin formato, sin diseño; y lo vamos a usar para probar algunos métodos que le pasen los valores de new Date(). Es evidente que si javascript devuelve los tiempos en número, lo más lógico es escribir esos números en el reloj ... y se terminó el problema. Pero, no. Porque cada diseñador le va a dar una transición o un dibujo distinto según su gusto, y entonces el movimiento de los números o las letras van a requerir un poco más de trabajo.

No voy a esmerarme con eso, hay una enorme cantidad de creativos que cambian la presentación de las horas con métodos más o menos usables, y centenares de páginas donde los exponen. Así que presento algunos muy básicos, como para que empiecen a crear los suyos usando alguno de modelo.

See the Pen Digital clock. (Not for production pages.) by Kseso (@Kseso) on CodePen.

Nota del Editor: curiosidad CSS, fíjate cómo consigue el autor del pen el gradiente en el borde del reloj.

Repito: no es para usar en una página real, porque está muy mal hecho. Es nada más para desarmarlo y estudiarlo.

/* CAPTURAMOS EL VALOR DE LOS MINUTOS */ var minuto = (new Date()).getMinutes(); /* CREAMOS UN ARRAY CON LOS DÍAS DE SEMANA */ var semana = ["DOM", "LUN", "MAR", "MIE", "JUE", "VIE", "SAB"]; /* CREAMOS UN ARRAY CON LOS MESES DEL AÑO */ var mes = ["ENE", "FEB", "MAR", "ABR", "MAY", "JUN", "JUL", "AGO", "SEP", "OCT", "NOV", "DIC"]; function tiempo() { /* CADENA VACÍA PARA PONER NÚMEROS DE 59 A 00 */ var listaMinSeg = ""; /* LLENAMOS LA CADENA DESDE 59 A 10 */ for(ms=59; ms>9; ms--) listaMinSeg += "
"+ms; /* COMPLETAMOS LA CADENA DESDE 09 A 00 */ for(ms=9; ms>-1; ms--) listaMinSeg += "
"+"0"+ms; /* PONEMOS EN EL CONTENEDOR DE SEGUNDOS LA LISTA DE NÚMEROS (AMPLIADA) */ document.querySelector("#segundo").innerHTML = "00" + listaMinSeg; /* PONEMOS EN EL CONTENEDOR DE MINUTOS LA MISMA LISTA DE NÚMEROS (DUPLICADA) */ document.querySelector("#minuto").innerHTML = listaMinSeg + listaMinSeg; /* TRASLADAMOS LA LISTA DE NÚMEROS HASTA EL VALOR ACTUAL DE MINUTOS */ document.querySelector("#minuto").style.transform = "translateY("+ (minuto-59) +"em)"; /* EJECUTAMOS LA FUNCIÓN QUE CARGA LOS DEMÁS VALORES DE TIEMPO */ ajustaHora(); } function ajustaHora() { /* CAPTURAMOS LOS VALORES DE FECHA */ fecha = new Date(); /* PONEMOS LA HORA EN LA CASILLA DE HORA */ document.querySelector("#hora").innerHTML = fecha.getHours(); /* PONEMOS EL ELEMENTO DE ARRAY QUE CORRESPONDE AL DÍA DE SEMANA */ document.querySelector("#semana").innerHTML = semana[fecha.getDay()]; /* PONEMOS EL DÍA DEL MES CORREGIDO A 2 DÍGITOS */ document.querySelector("#dia").innerHTML = ("0" + fecha.getDate()).slice(-2); /* PONEMOS EL ELEMENTO DE ARRAY QUE CORRESPONDE AL MES */ document.querySelector("#mes").innerHTML = mes[fecha.getMonth()]; /* PONEMOS EL AÑO EN LA CASILLA DE AÑO */ document.querySelector("#anyo").innerHTML = fecha.getFullYear(); /*ACTUALIZAMOS A CADA MINUTO */ setTimeout(ajustaHora, 60000); } /* EJECUTAMOS LA FUNCIÓN INICIAL AL CARGARSE EL HTML */ document.addEventListener("DOMContentLoaded", tiempo, false);

Como ven, la presentación de tiempos se divide en dos partes. En la primera tenemos unos segundos falsos, porque no los capturamos en ningún momento, y el display arranca desde "00" al cargarse la página sin importar cuáles sean lo segundos reales. Esto tiene un problema, y es que el reloj puede estar hasta 59 segundos atrasado. Pero los minutos ya los leemos correctamente. Vamos a los detalles.

  • Ponemos en una variable el valor de los minutos, y luego escribimos en las dos siguientes una lista (array) con los textos de cada día de semana y otra con los meses del año.
  • Ya en la función de inicio creamos una string vacía donde vamos a meter la serie de números para los minutos y los segundos. Son los que se van a desplazar con un mecanismo parecido a las agujas de los analógicos. El llenado se hace en dos etapas; primero van los números desde el 59 al 10, separados por una etiqueta break para que queden encolumnados; luego se agregan los números con formato "09" a "00". Y ya vemos otra de las formas de rellenar con ceros en listas prehechas para mantener el formato de "dos dígitos". Aunque por el funcionamiento práctico del escript, todavía hay que hacerles unos ajustes.
  • En los segundos se agrega al comienzo un "00", porque el CSS va a desplazar toda la tira paso a paso (de a 1/4 de segundo, para el caso) y al llegar a "59" se va a desplazar un paso más, y si no agregamos el doble cero se va a ver un espacio en blanco. Una vez que se acomoda, salta al comienzo de la tira de números, donde justamente está el otro doble cero, y a la vista el cambio será imperceptible cuando recomience la vuelta.
  • En los minutos se usa el truco de duplicar el contenido, y la lista aparece dos veces. La anterior debió ser igual, pero como no va a haber desplazamiento de ajuste según el tiempo real (recordemos: los segundos son falsos y solamente rotan de 0 a 59), alcanzó con un "00".
  • Por supuesto, ambas tiras de texto se meten en sus respectivos contenedores dentro del reloj como HTML, y el navegador los muestra como si estuvieran en el código fuente.
  • Ahora tomamos el valor de minuto capturado y lo usamos para trasladar sobre el eje "y" la tira de minutos. Aquí notamos mejor el método adaptado de las agujas, ya que movemos la tira hasta mostrar el número de minuto actual en la serie "de abajo" y CSS la va a desplazar por su margin-top los 60 pasos antes de recomenzar. Por eso se duplica el contenido; si la animación empieza (p.e.) en 30 minutos, va a pasar los "59" de la serie de abajo y empezar los "00" de la de arriba hasta los "30" de arriba, alli completa los keyframes y salta a su inicio de nuevo, que eran los "30" de la serie de abajo para empezar otra vez. Es decir, que hace un recorrido circular sinfín.
  • Hasta ahora estamos usando el motor CSS para animar el reloj mostrando dos métodos con desplazamiento, aunque los minutos cambien de un golpe y no se vean con transición entre números. Son solamente ejemplos, y a partir de ahora probamos cómo llenar el reloj con javascript. Para eso disparamos la siguente función que arranca llenando las horas.
  • Empezamos capturando la fecha en una variable, y usando innerHTML metemos el valor de las horas en el elemento correspondiente. Para el caso, si es anterior a las 10 de la mañana aparece el único número centrado.
  • fecha.getDay() es un número de 0 a 6, y lo usamos como índice del array que contiene los textos para cada día de la semana.
  • El día del mes coincide con el calendario, así que lo ponemos tal cual. Aunque aprovechamos para probar otro método de relleno de ceros. Éste es un poco más sofisticado: unaCadena.slice(inicio, fin) extrae una parte de la cadena de texto que empieza en el caracter de inicio y termina en el de fin; si no hay argumentos, empieza desde el comienzo y si falta el fin, corta después del último. Una característica muy particular es que el conteo es circular. Dijimos que si empezamos en cero va a cortar desde el comienzo, pero si ponemos -2 no da error, sino que sigue contando desde el último caracter, hacia el comienzo. Por eso si al día le agregamos siempre un "0" al principio el número puede quedar con 2 ó 3 dígitos, pero si cortamos los 2 últimos siempre vamos a tener un valor de dos dígitos y si originalmente era de uno, va a incluir el "0" agregado.
  • Con los meses trabajamos igual que los días de semana. Javascript devuelve un valor de 0 a 11, y lo usamos como índice del array de meses.
  • El año ya lo pedimos en formato de 4 dígitos, y va tal cual.
  • Como nuestra "unidad de tiempo" más chica es el minuto, recargamos esta función cada 60000 milisegundos y así actualizamos los valores una vez por minuto. Aunque ya vimos que los minutos y los segundos se mueven por CSS.
  • El último escuchador de eventos ejecuta la primer función al cargarse el HTML.

Para hacer animaciones en el cambio de los números y textos lo mejor es disparar una función cada segundo o minuto (éste último consume menos recursos) y que modifique valores de estilo para mover con transition.

Los segundos se pueden mover con keyframes, porque cambian rápido a la vista. Los minutos no pueden quedar a medio camino en 30 segundos, tienen que moverse de un salto; y eso no es fácil, habrá que escribir mucho porcentaje.

Pero ni hablar de que ahora que sabemos animar relojes, podemos agregar cucús, colosos golpeando campanas, alarmas de despertador vibrando, ...

Furoya: Autor del artículo

Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.