写个时钟(行为篇)

129 阅读6分钟

通过对样式篇源代码进行观察,我们会发现,只用一个 div 元素表盘,并使用对应的 before 和 after 两个伪元素时针分针,那么一共只能是三个元素

需要显示秒针和表盘上的刻度,我们需要更多的元素来展示。那么今天,我们就把秒针刻度显示出来,并且让时钟与真实的时间同步


一、显示表盘刻度

我们之前使用的是一个 div 元素显示表盘,为了便于控制,我们给表盘一个 class 值: 

<div class="dial"></div>

这样,在 css 中,就使用类选择器控制该 div

.dial {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 600px;
  height: 600px;
  margin: auto;
  border: 5px solid #fff;
  border-radius: 50%;
}
.dial::before,
.dial::after {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 275px;
  margin: auto;
  background: #fff;
  border-radius: 5px;
  content: "";
  transform-origin: 25px center;
}
.dial::before {
  width: 300px;
  height: 5px;
  animation: clock 1s linear infinite;
}
.dial::after {
  width: 200px;
  height: 10px;
  animation: clock 60s linear infinite;
}

表盘的刻度属于该 div 的子元素,并且有 60 个之多。于是我们通过 JavaScript 动态生成。

for (let i = 0; i < 60; i++) {
  const oSpan = document.createElement('span'); // 创建刻度元素
}

这样,我们通过循环创建了 60 个 span 元素。然后,依次把这 60 个 span 元素放到 div 中:

const oDiv = document.querySelector('.dial'); // 获取钟表盘面
for (let i = 0; i < 60; i++) {
  const oSpan = document.createElement('span'); // 创建刻度元素
  oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
}

然后,控制 span 元素的样式:

.dial > span {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  display: block;
  width: 5px;
  height: 10px;
  margin: auto;
  background: #fff;
}

然后,我们惊讶的看到了如下结果:

刻度1.png

惊讶之余,我们想了一下。创建 60 个 span 元素,只用同一套 css 控制css 中的定位控制只有一套,那么必然 60 个 span 元素重合了。

想要让这 60 个 span 元素围绕表盘平均排列,最好的做法就是让其围绕表盘中心旋转。于是我们设置 span 元素的旋转中心表盘中心

transform-origin: center 300px;

这样控制 span 的完整样式如下:

.dial > span {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  display: block;
  width: 5px;
  height: 10px;
  margin: auto;
  background: #fff;
  transform-origin: center 300px;
}

然后,我们在创建 span 元素的时候控制每一个 span 元素旋转角度

const oDiv = document.querySelector('.dial'); // 获取钟表盘面
for (let i = 0; i < 60; i++) {
  const oSpan = document.createElement('span'); // 创建刻度元素
  oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
  oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
}

于是我们得到如下效果:

刻度2.png

有那么一点点感觉了,但是不够。刻度是为了方便我们更快的读取时间,因此为了更好的用户体验,表盘刻度上每 5 个刻度,都是突出显示的。

于是,我们先定义一个用于突出显示的 span 元素的样式:

.dial > span.bold {
  width: 7.5px;
  height: 20px;
}

这样一来,我们就只需要每 5 个 span 元素就添加一个 class 的值为 bold 即可:

const oDiv = document.querySelector('.dial'); // 获取钟表盘面
for (let i = 0; i < 60; i++) {
  const oSpan = document.createElement('span'); // 创建刻度元素
  oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
  if (i % 5 === 0) {
    oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
  }
  oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
}

于是我们就得到了一个想要的表盘:

刻度3.png

最后,咱们把创建刻度的算法封装成一个函数:

/**
 * @description 创建刻度
*/
const createScale = () => {
  const oDiv = document.querySelector('.dial'); // 获取钟表盘面
  for (let i = 0; i < 60; i++) {
    const oSpan = document.createElement('span'); // 创建刻度元素
    oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
    if (i % 5 === 0) {
      oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
    }
    oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
  }
}

这样,我们只需要在网页加载的时候直接调用该函数即可:

window.onload = createScale;

至此,我们已经有了我们想要的表盘。


二、重新创建时针、分针和秒针

有两点是我们需要考虑的:

  1. 时针、分针、秒针,是三个并列的元素,所以决定不再用伪元素,而是创建三个并列的 div 元素
  2. 时针、分针、秒针都要根据真实时间不断旋转,既然在创建指针的时候已经获取了表盘的 div 元素,那么直接在 JavaScript 中创建这三个表示指针的 div 元素并加以控制,会更加方便。

于是,我们就写一个创建这三个指针元素的函数:

/**
 * @description 创建指针
 * @param {HTMLDIVElement} oDiv 钟表盘面元素
*/
const createPointer = (oDiv) => {
  const oHour = document.createElement('div'); // 创建小时指针元素
  const oMinute = document.createElement('div'); // 创建分钟指针元素
  const oSecond = document.createElement('div'); // 创建秒指针元素
}

指针的 div 元素,还是一样使用 css 控制其显示样式,于是先定义这三个指针的显示样式

.dial > .pointer {
  position: absolute;
  right: 0;
  left: 0;
  margin: auto;
  background: #fff;
  border-radius: 5px;
}
.dial > .hour {
  top: 125px;
  width: 10px;
  height: 200px;
  transform-origin: center 175px;
}
.dial > .minute {
  top: 25px;
  width: 7.5px;
  height: 300px;
  transform-origin: center 275px;
}
.dial > .second {
  top: 25px;
  width: 2.5px;
  height: 300px;
  transform-origin: center 275px;
}

指针的初始位置和旋转的中心点就不做展开讲解了,对“样式篇”的思路理解的,这里一看就懂。

没错,我们需要给每一个指针都设置一个 class 的值为 pointer。并且,需要分别时针、分针、秒针class 值添加 hour、minute、second 三个值以便单独控制显示样式:

/**
 * @description 创建指针
 * @param {HTMLDIVElement} oDiv 钟表盘面元素
*/
const createPointer = (oDiv) => {
  const oHour = document.createElement('div'); // 创建小时指针元素
  const oMinute = document.createElement('div'); // 创建分钟指针元素
  const oSecond = document.createElement('div'); // 创建秒指针元素
  oHour.className = 'pointer hour'; // 设置小时指针元素的 class 值
  oMinute.className = 'pointer minute'; // 设置分钟指针元素的 class 值
  oSecond.className = 'pointer second'; // 设置秒指针元素的 class 值
}

然后,我们还需要把这三个 div 元素都放到表盘 div 元素中:

/**
 * @description 创建指针
 * @param {HTMLDIVElement} oDiv 钟表盘面元素
*/
const createPointer = (oDiv) => {
  const oHour = document.createElement('div'); // 创建小时指针元素
  const oMinute = document.createElement('div'); // 创建分钟指针元素
  const oSecond = document.createElement('div'); // 创建秒指针元素
  oHour.className = 'pointer hour'; // 设置小时指针元素的 class 值
  oMinute.className = 'pointer minute'; // 设置分钟指针元素的 class 值
  oSecond.className = 'pointer second'; // 设置秒指针元素的 class 值
  oDiv.appendChild(oHour); // 将小时指针放到钟表盘面
  oDiv.appendChild(oMinute); // 将分钟指针放到钟表盘面
  oDiv.appendChild(oSecond); // 将秒指针放到钟表盘面
}

由于我们是在页面加载的时候调用了创建刻度createScale 函数,于是我们可以在 createScale 函数的末尾调用创建指针的 createPointer 函数:

/**
 * @description 创建刻度
*/
const createScale = () => {
  const oDiv = document.querySelector('.dial'); // 获取钟表盘面
  for (let i = 0; i < 60; i++) {
    const oSpan = document.createElement('span'); // 创建刻度元素
    oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
    if (i % 5 === 0) {
      oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
    }
    oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
  }
  createPointer(oDiv); // 创建指针
}

于是,我们得到了如下的静态时钟:

静态时钟.png

没错,这一篇章里我们需要考虑通过真实时间计算并控制指针旋转角度。于是乎指针的初始位置和前面“样式篇”的水平向右不同,放到了竖直向上


三、指针旋转并与真实时间同步

一样的思路,咱们先创建一个设置实时时间的函数吧:

/**
 * @description 设置实时时间
 * @param {HTMLDIVElement} oHour 小时指针元素
 * @param {HTMLDIVElement} oMinute 分钟指针元素
 * @param {HTMLDIVElement} oSecond 秒指针元素
*/
const setRealTime = (oHour, oMinute, oSecond) => {
  const now = new Date(); // 获取当前时间
  const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
}

时钟采用的是 12 小时制,于是每一个小时,时针旋转的度数为 30deg

oHour.style.transform = `rotate(${30 * hour}deg)`; // 根据当前小时设置小时指针的旋转角度

下午的小时数大于 12,这个时候计算出来的角度会大于 360deg,但是由于到了中午 12 点,时针将会回到原点,所以从显示层面,下午的小时度数不受影响。

一小时有 60 分钟,因此每一分钟,分针旋转角度为 6deg

oMinute.style.transform = `rotate(${6 * minute}deg)`; // 根据当前分钟设置分钟指针的旋转角度

同样,一分钟有 60 秒,因此每一秒钟,秒针旋转角度也为 6deg

oSecond.style.transform = `rotate(${6 * second}deg)`; // 根据当前秒设置秒指针的旋转角度

放到整个函数中:

/**
 * @description 设置实时时间
 * @param {HTMLDIVElement} oHour 小时指针元素
 * @param {HTMLDIVElement} oMinute 分钟指针元素
 * @param {HTMLDIVElement} oSecond 秒指针元素
*/
const setRealTime = (oHour, oMinute, oSecond) => {
  const now = new Date(); // 获取当前时间
  const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
  oHour.style.transform = `rotate(${30 * hour}deg)`; // 根据当前小时设置小时指针的旋转角度
  oMinute.style.transform = `rotate(${6 * minute}deg)`; // 根据当前分钟设置分钟指针的旋转角度
  oSecond.style.transform = `rotate(${6 * second}deg)`; // 根据当前秒设置秒指针的旋转角度  setTimeout(() => setRealTime(oHour, oMinute, oSecond), 1000); // 每隔一秒钟更新一次时间及其指针旋转角度
}

同样的道理,我们可以在创建指针createPointer 函数的末尾调用设置实时时间setRealTime 函数。

/**
 * @description 创建指针
 * @param {HTMLDIVElement} oDiv 钟表盘面元素
*/
const createPointer = (oDiv) => {
  const oHour = document.createElement('div'); // 创建小时指针元素
  const oMinute = document.createElement('div'); // 创建分钟指针元素
  const oSecond = document.createElement('div'); // 创建秒指针元素
  oHour.className = 'pointer hour'; // 设置小时指针元素的 class 值
  oMinute.className = 'pointer minute'; // 设置分钟指针元素的 class 值
  oSecond.className = 'pointer second'; // 设置秒指针元素的 class 值
  oDiv.appendChild(oHour); // 将小时指针放到钟表盘面
  oDiv.appendChild(oMinute); // 将分钟指针放到钟表盘面
  oDiv.appendChild(oSecond); // 将秒指针放到钟表盘面
  setRealTime(oHour, oMinute, oSecond); // 设置实时时间
}

于是,我们得到了如下结果:

当前时间.png

最后,我们需要让时钟动起来。该怎么做呢?

设置实时时间setRealTime 函数中,已经完成了获取当前时间并让指针显示的功能。于是我们直接在这个 setRealTime 函数中,每一秒钟递归调用一次该函数,就可以实现时钟的走动:

/**
 * @description 设置实时时间
 * @param {HTMLDIVElement} oHour 小时指针元素
 * @param {HTMLDIVElement} oMinute 分钟指针元素
 * @param {HTMLDIVElement} oSecond 秒指针元素
*/
const setRealTime = (oHour, oMinute, oSecond) => {
  const now = new Date(); // 获取当前时间
  const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
  oHour.style.transform = `rotate(${30 * hour}deg)`; // 根据当前小时设置小时指针的旋转角度
  oMinute.style.transform = `rotate(${6 * minute}deg)`; // 根据当前分钟设置分钟指针的旋转角度
  oSecond.style.transform = `rotate(${6 * second}deg)`; // 根据当前秒设置秒指针的旋转角度
  setTimeout(() => setRealTime(oHour, oMinute, oSecond), 1000); // 每隔一秒钟更新一次时间及其指针旋转角度
}

这样,我们通过 setTimeout() 方法,设置延迟 1000ms 调用一次设置实时时间setRealTime 函数,通过巧妙的递归,完成了时钟的走动。

clock2.gif

这样,我们就把一个外观相对逼真,时间展示真实时间的时钟写出来了!


完整源码

附上完整源码,拿走不谢O(∩_∩)O

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clock</title>
<style type="text/css">
body {
  background: #333;
}
.dial {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 600px;
  height: 600px;
  margin: auto;
  border: 5px solid #fff;
  border-radius: 50%;
}
.dial > span {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  display: block;
  width: 5px;
  height: 10px;
  margin: auto;
  background: #fff;
  transform-origin: center 300px;
}
.dial > span.bold {
  width: 7.5px;
  height: 20px;
}
.dial > .pointer {
  position: absolute;
  right: 0;
  left: 0;
  margin: auto;
  background: #fff;
  border-radius: 5px;
}
.dial > .hour {
  top: 125px;
  width: 10px;
  height: 200px;
  transform-origin: center 175px;
}
.dial > .minute {
  top: 25px;
  width: 7.5px;
  height: 300px;
  transform-origin: center 275px;
}
.dial > .second {
  top: 25px;
  width: 2.5px;
  height: 300px;
  transform-origin: center 275px;
}
</style>
<script type="text/javascript">
/**
 * @description 创建刻度
*/
const createScale = () => {
  const oDiv = document.querySelector('.dial'); // 获取钟表盘面
  for (let i = 0; i < 60; i++) {
    const oSpan = document.createElement('span'); // 创建刻度元素
    oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
    if (i % 5 === 0) {
      oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
    }
    oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
  }
  createPointer(oDiv); // 创建指针
}

/**
 * @description 创建指针
 * @param {HTMLDIVElement} oDiv 钟表盘面元素
*/
const createPointer = (oDiv) => {
  const oHour = document.createElement('div'); // 创建小时指针元素
  const oMinute = document.createElement('div'); // 创建分钟指针元素
  const oSecond = document.createElement('div'); // 创建秒指针元素
  oHour.className = 'pointer hour'; // 设置小时指针元素的 class 值
  oMinute.className = 'pointer minute'; // 设置分钟指针元素的 class 值
  oSecond.className = 'pointer second'; // 设置秒指针元素的 class 值
  oDiv.appendChild(oHour); // 将小时指针放到钟表盘面
  oDiv.appendChild(oMinute); // 将分钟指针放到钟表盘面
  oDiv.appendChild(oSecond); // 将秒指针放到钟表盘面
  setRealTime(oHour, oMinute, oSecond); // 设置实时时间
}

/**
 * @description 设置实时时间
 * @param {HTMLDIVElement} oHour 小时指针元素
 * @param {HTMLDIVElement} oMinute 分钟指针元素
 * @param {HTMLDIVElement} oSecond 秒指针元素
*/
const setRealTime = (oHour, oMinute, oSecond) => {
  const now = new Date(); // 获取当前时间
  const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
  oHour.style.transform = `rotate(${30 * hour}deg)`; // 根据当前小时设置小时指针的旋转角度
  oMinute.style.transform = `rotate(${6 * minute}deg)`; // 根据当前分钟设置分钟指针的旋转角度
  oSecond.style.transform = `rotate(${6 * second}deg)`; // 根据当前秒设置秒指针的旋转角度
  setTimeout(() => setRealTime(oHour, oMinute, oSecond), 1000); // 每隔一秒钟更新一次时间及其指针旋转角度
}

window.onload = createScale;
</script>
</head>
<body>
  <div class="dial"></div>
</body>
</html>

由于时针、分针、秒针都是通过 JavaScript 创建的,导致 DOM 控制的成本较高,也让 JavaScript 的代码较多。于是,我们可以尝试使用 CSS4 中的变量进行优化。尽情期待我们的“写个时钟(进阶篇)”!