¿Alguna vez ha necesitado un temporizador de cuenta atrás en un proyecto? Para algo así, puede ser natural buscar un complemento, pero en realidad es mucho más sencillo crear uno de lo que piensas y solo requiere la trifecta de HTML, CSS y JavaScript. ¡Hagamos uno juntos!
Esto es lo que buscamos:
Aquí hay algunas cosas que hace el temporizador que cubriremos en esta publicación:
- Muestra el tiempo restante inicial
- Convierte el valor de tiempo en un
MM:SS
formato - Calcula la diferencia entre el tiempo restante inicial y el tiempo transcurrido
- Cambia de color a medida que el tiempo restante se acerca a cero
- Muestra el progreso del tiempo restante como un anillo animado.
OK, eso es lo que queremos, ¡así que hagámoslo realidad!
Paso 1: comience con el marcado y los estilos básicos
Comencemos creando una plantilla básica para nuestro temporizador. Agregaremos un svg con un elemento circular adentro para dibujar un anillo de temporizador que indicará el tiempo que pasa y agregaremos un intervalo para mostrar el valor de tiempo restante. Tenga en cuenta que estamos escribiendo el HTML en JavaScript e inyectando en el DOM apuntando al #app
elemento. Claro, podríamos mover mucho a un archivo HTML, si eso es lo tuyo.
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45" />
</g>
</svg>
<span>
<!-- Remaining time label -->
</span>
</div>
`;
Ahora que tenemos algunas marcas con las que trabajar, modifiquémoslo un poco para que tengamos una buena imagen para empezar. Específicamente, vamos a:
- Establecer el tamaño del temporizador
- Elimina el relleno y el trazo del elemento de envoltura del círculo para que obtengamos la forma pero dejamos que se vea el tiempo transcurrido.
- Establecer el ancho y el color del anillo
/* Sets the containers height and width */
.base-timer {
position: relative;
height: 300px;
width: 300px;
}
/* Removes SVG styling that would hide the time label */
.base-timer__circle {
fill: none;
stroke: none;
}
/* The SVG path that displays the timer's progress */
.base-timer__path-elapsed {
stroke-width: 7px;
stroke: grey;
}
Una vez hecho esto, terminamos con una plantilla básica que se ve así.
Paso 2: configuración de la etiqueta de tiempo
Como probablemente haya notado, la plantilla incluye un vacío que mantendrá el tiempo restante. Llenaremos ese lugar con un valor adecuado. Dijimos antes que el tiempo estará en MM:SS
formato. Para hacer eso crearemos un método llamado formatTimeLeft
:
function formatTimeLeft(time) {
// The largest round integer less than or equal to the result of time divided being by 60.
const minutes = Math.floor(time / 60);
// Seconds are the remainder of the time divided by 60 (modulus operator)
let seconds = time % 60;
// If the value of seconds is less than 10, then display seconds with a leading zero
if (seconds < 10) {
seconds = `0${seconds}`;
}
// The output in MM:SS format
return `${minutes}:${seconds}`;
}
Luego usaremos nuestro método en la plantilla:
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
</g>
</svg>
<span id="base-timer-label" class="base-timer__label">
${formatTime(timeLeft)}
</span>
</div>
`
Para mostrar el valor dentro del anillo, necesitamos actualizar un poco nuestros estilos.
.base-timer__label {
position: absolute;
/* Size should match the parent container */
width: 300px;
height: 300px;
/* Keep the label aligned to the top */
top: 0;
/* Create a flexible box that centers content vertically and horizontally */
display: flex;
align-items: center;
justify-content: center;
/* Sort of an arbitrary number; adjust to your liking */
font-size: 48px;
}
OK, estamos listos para jugar con el timeLeft
valor, pero el valor aún no existe. Creámoslo y establezcamos el valor inicial en nuestro límite de tiempo.
// Start with an initial value of 20 seconds
const TIME_LIMIT = 20;
// Initially, no time has passed, but this will count up
// and subtract from the TIME_LIMIT
let timePassed = 0;
let timeLeft = TIME_LIMIT;
Y estamos un paso más cerca.
¡Tocar el asunto exacto! Ahora tenemos un temporizador que comienza a los 20 segundos … pero todavía no cuenta nada. Démosle vida para que cuente hasta cero segundos.
Paso 3: cuenta regresiva
Pensemos en lo que necesitamos para contar el tiempo. Ahora mismo, tenemos un timeLimit
valor que representa nuestro tiempo inicial, y un timePassed
valor que indica cuánto tiempo ha pasado una vez que comienza la cuenta atrás.
Lo que tenemos que hacer es incrementar el valor de timePassed
por una unidad por segundo y vuelva a calcular el timeLeft
valor basado en el nuevo timePassed
valor. Podemos lograr eso usando el setInterval
función.
Implementemos un método llamado startTimer
esa voluntad:
- Establecer intervalo de contador
- Incrementar el
timePassed
valor cada segundo - Vuelva a calcular el nuevo valor de
timeLeft
- Actualizar el valor de la etiqueta en la plantilla
También necesitamos mantener la referencia a ese objeto de intervalo para borrarlo cuando sea necesario; es por eso que crearemos un timerInterval
variable.
let timerInterval = null;
document.getElementById("app").innerHTML = `...`
function startTimer() {
timerInterval = setInterval(() => {
// The amount of time passed increments by one
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
// The time left label is updated
document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
}, 1000);
}
Tenemos un método que inicia el temporizador pero no lo llamamos en ninguna parte. Iniciemos nuestro temporizador inmediatamente en carga.
document.getElementById("app").innerHTML = `...`
startTimer();
¡Eso es! Nuestro temporizador ahora contará el tiempo. Si bien eso es genial y todo, sería mejor si pudiéramos agregar algo de color al anillo alrededor de la etiqueta de tiempo y cambiar el color en diferentes valores de tiempo.
Paso 4: cubra el anillo del temporizador con otro anillo
Para visualizar el paso del tiempo, necesitamos agregar una segunda capa a nuestro anillo que maneja la animación. Lo que estamos haciendo es esencialmente apilar un nuevo anillo verde sobre el anillo gris original para que el anillo verde se anime para revelar el anillo gris a medida que pasa el tiempo, como una barra de progreso.
Primero agreguemos un elemento de ruta en nuestro elemento SVG.
document.getElementById("app").innerHTML = `
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span id="base-timer-label" class="base-timer__label">
${formatTime(timeLeft)}
</span>
</div>
`;
A continuación, creemos un color inicial para la ruta de tiempo restante.
const COLOR_CODES = {
info: {
color: "green"
}
};
let remainingPathColor = COLOR_CODES.info.color;
Finalmente, agreguemos algunos estilos para que la ruta circular se vea como nuestro anillo gris original. Lo importante aquí es asegurarse de que stroke-width
es del mismo tamaño que el anillo original y que la duración del transition
se establece en un segundo para que se anime sin problemas y se corresponda con el tiempo restante en la etiqueta de tiempo.
.base-timer__path-remaining {
/* Just as thick as the original ring */
stroke-width: 7px;
/* Rounds the line endings to create a seamless circle */
stroke-linecap: round;
/* Makes sure the animation starts at the top of the circle */
transform: rotate(90deg);
transform-origin: center;
/* One second aligns with the speed of the countdown timer */
transition: 1s linear all;
/* Allows the ring to change color when the color value updates */
stroke: currentColor;
}
.base-timer__svg {
/* Flips the svg and makes the animation to move left-to-right */
transform: scaleX(-1);
}
Esto generará un trazo que cubre el anillo del temporizador como debería, pero no se anima todavía para revelar el anillo del temporizador a medida que pasa el tiempo.
Para animar la longitud de la línea de tiempo restante, usaremos el stroke-dasharray
propiedad. Chris explica cómo se usa para crear la ilusión de que un elemento “dibuja” en sí mismo. Y hay más detalles sobre la propiedad y ejemplos de ella en el almanaque CSS-Tricks.
Paso 5: anima el anillo de progreso
Veamos cómo quedará nuestro anillo con diferentes stroke-dasharray
valores:
Lo que podemos ver es que el valor de stroke-dasharray
en realidad, está cortando nuestro anillo de tiempo restante en secciones de igual longitud, donde la longitud es el valor de tiempo restante. Eso está sucediendo cuando establecemos el valor de stroke-dasharray
a un número de un solo dígito (es decir, 1-9).
El nombre dasharray sugiere que podemos establecer varios valores como una matriz. Veamos cómo se comportará si ponemos dos números en lugar de uno; en este caso, esos valores son 10 y 30.
stroke-dasharray: 10 30
Eso establece la longitud de la primera sección (tiempo restante) en 10 y la segunda sección (tiempo transcurrido) en 30. Podemos usar eso en nuestro temporizador con un pequeño truco. Lo que necesitamos inicialmente es que el anillo cubra toda la longitud del círculo, lo que significa que el tiempo restante es igual a la longitud de nuestro anillo.
¿Cuál es esa longitud? Obtenga su antiguo libro de texto de geometría, porque podemos calcular la longitud de un arco con algunas matemáticas:
Length = 2πr = 2 * π * 45 = 282,6
Ese es el valor que queremos usar cuando el anillo se montó inicialmente. Veamos como queda.
stroke-dasharray: 283 283
¡Eso funciona!
Bien, el primer valor en la matriz es nuestro tiempo restante, y el segundo marca cuánto tiempo ha pasado. Lo que tenemos que hacer ahora es manipular el primer valor. Veamos a continuación qué podemos esperar cuando cambiamos el primer valor.
Crearemos dos métodos, uno responsable de calcular qué fracción del tiempo inicial queda, y otro responsable de calcular el stroke-dasharray
valorar y actualizar el <path>
elemento que representa nuestro tiempo restante.
// Divides time left by the defined time limit.
function calculateTimeFraction() {
return timeLeft / TIME_LIMIT;
}
// Update the dasharray value as time passes, starting with 283
function setCircleDasharray() {
const circleDasharray = `${(
calculateTimeFraction() * FULL_DASH_ARRAY
).toFixed(0)} 283`;
document
.getElementById("base-timer-path-remaining")
.setAttribute("stroke-dasharray", circleDasharray);
}
También necesitamos actualizar nuestra ruta cada segundo que pasa. Eso significa que debemos llamar al recién creado setCircleDasharray
método dentro de nuestro timerInterval
.
function startTimer() {
timerInterval = setInterval(() => {
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
setCircleDasharray();
}, 1000);
}
¡Ahora podemos ver que las cosas se mueven!
Vaya, funciona … pero … mira de cerca, especialmente al final. Parece que nuestra animación se retrasa un segundo. Cuando llegamos a 0, todavía se ve una pequeña parte del anillo.
Esto se debe a que la duración de la animación se establece en un segundo. Cuando el valor del tiempo restante se establece en cero, todavía se necesita un segundo para animar el anillo a cero. Podemos deshacernos de eso reduciendo la longitud del anillo gradualmente durante la cuenta regresiva. Hacemos eso en nuestro calculateTimeFraction
método.
function calculateTimeFraction() {
const rawTimeFraction = timeLeft / TIME_LIMIT;
return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
}
¡Aquí vamos!
Vaya … hay una cosa más. Dijimos que queríamos cambiar el color del indicador de progreso cuando el tiempo restante alcance ciertos puntos, algo así como hacerle saber al usuario que el tiempo casi se acaba.
Paso 6: cambia el color de progreso en determinados momentos
Primero, necesitamos agregar dos umbrales que indicarán cuándo debemos cambiar a los estados de advertencia y alerta y agregar colores para cada uno de esos estados. Comenzamos con el verde, luego pasamos al naranja como advertencia, seguido del rojo cuando se acaba el tiempo.
// Warning occurs at 10s
const WARNING_THRESHOLD = 10;
// Alert occurs at 5s
const ALERT_THRESHOLD = 5;
const COLOR_CODES = {
info: {
color: "green"
},
warning: {
color: "orange",
threshold: WARNING_THRESHOLD
},
alert: {
color: "red",
threshold: ALERT_THRESHOLD
}
};
Ahora, creemos un método que sea responsable de verificar si se excedió el umbral y cambiar el color de progreso cuando eso suceda.
function setRemainingPathColor(timeLeft) {
const { alert, warning, info } = COLOR_CODES;
// If the remaining time is less than or equal to 5, remove the "warning" class and apply the "alert" class.
if (timeLeft <= alert.threshold) {
document
.getElementById("base-timer-path-remaining")
.classList.remove(warning.color);
document
.getElementById("base-timer-path-remaining")
.classList.add(alert.color);
// If the remaining time is less than or equal to 10, remove the base color and apply the "warning" class.
} else if (timeLeft <= warning.threshold) {
document
.getElementById("base-timer-path-remaining")
.classList.remove(info.color);
document
.getElementById("base-timer-path-remaining")
.classList.add(warning.color);
}
}
Entonces, básicamente estamos eliminando una clase CSS cuando el temporizador llega a un punto y agregando otra en su lugar. Necesitaremos definir esas clases.
.base-timer__path-remaining.green {
color: rgb(65, 184, 131);
}
.base-timer__path-remaining.orange {
color: orange;
}
.base-timer__path-remaining.red {
color: red;
}
Voilà, ahí lo tenemos. Aquí está la demostración nuevamente con todo junto.