如何使用 HTML、CSS 和 JavaScript 创建动画倒计时器

Avatar of Mateusz Rybczonek
Mateusz Rybczonek

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

您是否曾经在项目中需要一个倒计时器?对于类似的事情,使用插件可能是自然的选择,但实际上,创建一个倒计时器比您想象的要简单得多,只需要 HTML、CSS 和 JavaScript 三剑客即可。让我们一起创建一个吧!

这是我们的目标

以下是我们在本文中将介绍的计时器的一些功能

  • 显示剩余的初始时间
  • 将时间值转换为 MM:SS 格式
  • 计算剩余的初始时间与已过去时间的差值
  • 随着剩余时间接近零,颜色发生变化
  • 将剩余时间的进度显示为动画环

好的,这就是我们想要的,所以让我们开始吧!

步骤 1:从基本的标记和样式开始

让我们从为我们的计时器创建一个基本模板开始。我们将添加一个带有 svg 元素的 circle,以绘制一个指示时间流逝的计时器环,并添加一个 span 来显示剩余的时间值。请注意,我们正在 JavaScript 中编写 HTML 并将其注入 DOM,方法是定位 #app 元素。当然,如果更符合您的习惯,我们可以将其中许多内容移动到 HTML 文件中。

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>
`;

现在我们有一些标记可以使用了,让我们对其进行一些样式设置,以便我们有一个良好的视觉起点。具体来说,我们将

  • 设置计时器的大小
  • 删除圆形包装元素的填充和描边,以便我们获得形状,但让经过的时间显示出来
  • 设置环的宽度和颜色
/* 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;
}

完成这些操作后,我们将得到一个如下所示的基本模板。

步骤 2:设置时间标签

您可能已经注意到,模板包含一个空的 <span>,它将保存剩余的时间。我们将用适当的值填充该位置。我们之前说过,时间将采用 MM:SS 格式。为此,我们将创建一个名为 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}`;
}

然后,我们将在模板中使用我们的方法

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>
`

为了在环内显示值,我们需要稍微更新一下我们的样式。

.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;
}

好的,我们准备使用 timeLeft 值了,但该值尚不存在。让我们创建它并将初始值设置为我们的时间限制。

// 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;

我们又近了一步。

没错!现在我们有一个从 20 秒开始的计时器……但它还没有进行任何计数。让我们让它动起来,使其倒计时到零秒。

步骤 3:倒计时

让我们考虑一下我们需要做什么才能倒计时。现在,我们有一个 timeLimit 值,它表示我们的初始时间,以及一个 timePassed 值,它指示倒计时开始后经过了多长时间。

我们需要做的是每秒将 timePassed 值增加一个单位,并根据新的 timePassed 值重新计算 timeLeft 值。我们可以使用 setInterval 函数来实现这一点。

让我们实现一个名为 startTimer 的方法,该方法将

  • 设置计数器间隔
  • 每秒递增 timePassed
  • 重新计算 timeLeft 的新值
  • 更新模板中的标签值

我们还需要保留对该间隔对象的引用,以便在需要时清除它——这就是我们将创建一个 timerInterval 变量的原因。

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);
}

我们有一个启动计时器的方法,但我们没有在任何地方调用它。让我们在加载时立即启动我们的计时器。

document.getElementById("app").innerHTML = `...`
startTimer();

就是这样!我们的计时器现在将倒计时。虽然这很棒,但如果我们可以在时间标签周围的环上添加一些颜色,并在不同的时间值时更改颜色,那就更好了。

步骤 4:用另一个环覆盖计时器环

为了可视化时间流逝,我们需要在我们的环上添加一个第二层来处理动画。我们所做的本质上是在原始灰色环的顶部叠加一个新的绿色环,以便绿色环随着时间的推移进行动画以显示灰色环,就像进度条一样。

让我们首先在我们的 SVG 元素中添加一个 path 元素。

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>
`;

接下来,让我们为剩余时间路径创建一个初始颜色。

const COLOR_CODES = {
  info: {
    color: "green"
  }
};

let remainingPathColor = COLOR_CODES.info.color;

最后,让我们添加一些样式,使圆形路径看起来像我们最初的灰色环。这里重要的是要确保 stroke-width 与原始环的大小相同,并且 transition 的持续时间设置为一秒,以便它平滑地进行动画并与时间标签中的剩余时间相对应。

.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);
}

这将输出一个覆盖计时器环的描边,就像它应该的那样,但它还没有进行动画以随着时间的推移显示计时器环。

为了动画化剩余时间线的长度,我们将使用 stroke-dasharray 属性。Chris 解释了如何使用它来创建元素“绘制”自身的错觉。并且在 CSS-Tricks 年鉴中 详细介绍了该属性 及其示例。

步骤 5:动画化进度环

让我们看看我们的环在使用不同的 stroke-dasharray 值时会是什么样子

我们可以看到,stroke-dasharray 的值实际上将我们的剩余时间环分割成等长部分,其中长度为剩余时间值。当我们将 stroke-dasharray 的值设置为一位数(即 1-9)时,就会发生这种情况。

名称 dasharray 表明我们可以将多个值设置为数组。让我们看看如果我们设置两个数字而不是一个数字,它会如何表现;在这种情况下,这些值为 10 和 30。

stroke-dasharray: 10 30

这将第一部分(剩余时间)长度设置为 10,第二部分(已过去时间)长度设置为 30。我们可以通过一个小技巧在我们的计时器中使用它。我们最初需要的是让环覆盖圆的整个长度,这意味着剩余时间等于我们环的长度。

那个长度是多少?拿出你旧的几何教科书,因为我们可以用一些数学来计算弧长

Length = 2πr = 2 * π * 45 = 282,6

这就是我们想要在环最初挂载时使用的值。让我们看看它是什么样子。

stroke-dasharray: 283 283

这有效!

好的,数组中的第一个值是我们的剩余时间,第二个值标记已过去的时间。我们现在需要做的是操作第一个值。让我们在下面看看当我们更改第一个值时可以预期什么。

我们将创建两个方法,一个负责计算剩余初始时间的几分之几,另一个负责计算 stroke-dasharray 值并更新表示我们剩余时间的 <path> 元素。

// 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);
}

我们还需要在每秒经过时更新我们的路径。这意味着我们需要在我们的 timerInterval 中调用新创建的 setCircleDasharray 方法。

function startTimer() {
  timerInterval = setInterval(() => {
    timePassed = timePassed += 1;
    timeLeft = TIME_LIMIT - timePassed;
    document.getElementById("base-timer-label").innerHTML = formatTime(timeLeft);
    
    setCircleDasharray();
  }, 1000);
}

现在我们可以看到事物在移动了!

哇哦,它起作用了……但是……仔细看看,尤其是在最后。看起来我们的动画延迟了一秒。当我们达到 0 时,仍然可以看到一小部分环。

这是因为动画持续时间设置为一秒。当剩余时间值设置为零时,它仍然需要一秒才能真正将环动画设置为零。我们可以通过在倒计时期间逐渐减小环的长度来消除这种情况。我们在 calculateTimeFraction 方法中执行此操作。

function calculateTimeFraction() {
  const rawTimeFraction = timeLeft / TIME_LIMIT;
  return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
}

就是这样!

糟糕……还有一件事。我们说我们想在剩余时间达到某些点时更改进度指示器的颜色——有点像让用户知道时间快到了。

步骤 6:在特定时间点更改进度颜色

首先,我们需要添加两个阈值,用于指示何时应更改为警告和警报状态,并为每个状态添加颜色。我们从绿色开始,然后变为橙色作为警告,当时间即将用完时变为红色。

// 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
  }
};

现在,让我们创建一个负责检查阈值是否超过并更改进度颜色的方法。

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);
  }
}

因此,我们基本上是在计时器到达某个点时删除一个 CSS 类,并将其替换为另一个类。我们需要定义这些类。

.base-timer__path-remaining.green {
  color: rgb(65, 184, 131);
}

.base-timer__path-remaining.orange {
  color: orange;
}

.base-timer__path-remaining.red {
  color: red;
}

瞧,就是这样。以下是将所有内容组合在一起的演示。