JavaScript牛刀小试,结合CSS3动画属性来做一个系统时间同步的时钟

6,506 阅读11分钟

JavaScript总算入门了,复杂的就先不来了,今儿牛刀小试,先来一个系统同步的时钟效果,只用到最简单的获取系统时间的函数。因为学习是需要正反馈的,否则总也看不到效果,难免失了深入下去的兴趣。在之前的一篇专栏文章《利用CSS3的animation step属性实现wifi动画》的末尾,曾经做过一个时钟,当时鉴于JS空白,最终虽然也实现了时钟效果,但却无法做到与系统时间同步。今次,就来做一个可与当前时间完全一致的时钟,并一步步实现从简陋到豪华的华丽变身。

1. 基础版,先实现效果

为了避免复杂的时钟图层太多的干扰,所以先来一个最最基础的版本,一个圆代表底盘,三个直线分别代表时针分针秒针,嗯,或者你可以称之为极简主义(MUJI风?Maybe)。既然是基础图形,那可以不用借助Ai,直接手动码出来就可以了。

DOM部分(以800*600的画布为例,原点为(400,300))

<!--圆形底盘-->
<circle class="base" cx="400" cy="300" r="200" /> 
<!--长度为180的秒针-->
<line class="pointer" id="second" x1="400" y1="300" x2="400" y2="120"/>
<!--长度为140的分针-->
<line class="pointer" id="minute" x1="400" y1="300" x2="400" y2="160"/>
<!--长度为80的时针-->
<line class="pointer" id="hour" x1="400" y1="300" x2="400" y2="220"/>

CSS部分

<style type="text/css">
/* 圆形底盘 */
.base{stroke:#000000;stroke-width:4; fill:#FFFFFF}
/* 指针公用描边样式 */
.pointer{opacity:0.5;fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
/* 描边粗为6的秒针 */
#second{stroke-width:6;}
/* 描边粗为12的分针 */
#minute{stroke-width:12;}
/* 描边粗为18的时针 */
#hour{stroke-width:18;}
</style>

自然,这个最终的样子不是那么美,现在所有的指针都指向一个初始位置,即零时。

基础时钟效果

2.获取当前时间在SVG图形中体现

在此篇文章之前,一直玩的都是SVG+CSS3动画,那这里要啰嗦一下了。SVG毕竟只是一种图形绘制方法,支持CSS控制样式,但和控制交互的JS并无半点关联,既然要用JavaScript的函数及操作,所以,这里要做的第一步,就是把上一步创建的SVG图形像普通图片一样,整个一股脑儿的塞到html文件的body部分中。但这里SVG与普通图片相比,最大的优势是里面的标签元素可以被获取到并改变,往下看便知。在实际应用的过程中,要把SVG文件和JavaScript以引入的形式使用,这里为了方便,暂时不做剥离。

<!DOCTYPE html>
<html lang="en">
<svg>
…… SVG部分
</svg>
</html>

获取当前时间的函数很简单

<script>
var dt=new Date();
//获取小时
var hour=dt.getHours();
//获取分
var minute=dt.getMinutes();
//获取秒
var second=dt.getSeconds();
</script>

下面要做的是,把返回的时间在时钟上正确的映射,也就是说把旋转的度数与时间一一对应。360度对应12个小时,60分钟,60秒,就相应得出1hour→30度,1minute→6度,1second→6度的关系。为了方便使用,我把以上JS改了一下,直接改成度数值。

//小时对应的旋转度数
var hourDeg=dt.getHours()*30;
//分钟对应的旋转度数
var minuteDeg=dt.getMinutes()*6;
//秒对应的旋转度数
var secondDeg=dt.getSeconds()*6;

CSS3里支持基本的旋转变形的属性,transform:rotate(相应度数),所以这里定义一个函数来给指针增加类样式,旋转的度数为上面获取到的当前时间对应的旋转度数。

//定义同步时间的函数
function syn(){
//给时针追加旋转变形的类样式
$("hour").style.cssText="transform:rotate("+hourDeg+"deg);";
//给分针追加旋转变形的类样式
$("minute").style.cssText="transform:rotate("+minuteDeg+"deg);";
//给秒针追加旋转变形的类样式
$("second").style.cssText="transform:rotate("+secondDeg+"deg);";
}

这里还有一个和旋转变形关系极为密切的属性,旋转的原点transform-origin,因为是三种指针的共用属性值,所以我把旋转原点的定义transform-origin:400px 300px直接追加到了pointer类样式中。

为了测试是否好用,我让系统的时间暂时在窗口显示。

时钟时间与系统时间同步

当调用这个函数时,可以看到当前时间与SVG图形中的时针分针秒针是完全对应的。

3.让时钟不停歇的动起来

其实上面的效果实现后,会一丢丢javascript的小伙伴已经知道了如何让时钟不停歇的动起来了。那就是定时器功能。只要设置每秒触发一次定时器就可以了。

//定义获取当前时间的函数
function timeId(){
var dt=new Date();
var hourDeg=dt.getHours()*30;
var minuteDeg=dt.getMinutes()*6;
var secondDeg=dt.getSeconds()*6;
$("hour").style.cssText="transform:rotate("+hourDeg+"deg);";
$("minute").style.cssText="transform:rotate("+minuteDeg+"deg);";
$("second").style.cssText="transform:rotate("+secondDeg+"deg);";
}
//定时器启动前先调用一次函数
timeId();
//定义每秒触发一次的定时器
setInterval(timeId, 1000);

不停的时钟

这样的话,就得到了一枚丑丑的,但是可以与当前时间同步的时钟。 但是,这里有个小问题, 因为返回的系统时间是整数值,对于秒针而言,一格格的跳着走没什么问题,但对于分针尤其是时针而言,到了整点再跳一格,显然是与现实世界中的时钟不符的。 比如08:59:59在下一秒09:00:00时,时针会从8大幅度的跳向9。因为我的表盘没有指针,所以看上去并不是那么明显。但从截取的GIF中可以看到明显的分针从42跳到43的效果。
以时针为例,解决思路很简单,只需要把分钟返回的数值换算成时针相应的度数,与时针对应的度数相加即可。60minutes→1hour→30度,即1minute→0.5度。每1分钟,时针增加0.5度。同理,每1秒钟,分针增加0.1度。于是我把原来定义的时针和分针的旋转变形角度,改成了下面这种

//时针度数修正,增加分针对应的度数
var hourDeg=dt.getHours()*30 + dt.getMinutes()*0.5;
//分针度数修正,增加秒针对应的度数
var minuteDeg=dt.getMinutes()*6+dt.getSeconds()*0.1;

按接近整点截了一段动图,看一下效果

修正后的时钟效果
实现了平滑过渡。这种方法的实现,其实并没有用到CSS3的动画属性,只用到了旋转变形属性。codepen附上全部源码 base template of SVG+javascript clock

4.扔掉定时器,使用animation属性

不要急不要急,虽然上面完成了一个完全同步的时钟效果,但我们知道CSS3的animation属性是很强大的,结合 @keyframes 动画规则定义,是可以实现旋转效果的。在上面的实现思路中,因为每一秒完成一次旋转变形,因此并没有用到animation属性,下面就来试一试,如果不用定时器,如何实现这种效果。

先来看一下,如果是普通的旋转动画,不考虑动态的起始位置的话,定义的秒针的动画规则及animation属性的方法如下:

/*定义动画规则,旋转360度*/
@keyframes second{
0%{transform:rotate(0deg)}
100%{transform:rotate(360deg)}
}
/*定义秒针的旋转动画属性,360度需要时间60秒*/
.second{animation:second linear 60s infinite}

这里只需要解决一个问题,就是 0%关键帧对应的初始旋转角度的问题 。0%的确认后,100%的只需要增加360度即可。问题难就难在javascript并不能把变量值传进去来改变CSS的属性,但好在可以通过javascrpt重写CSS属性值。这里实现的思路有些麻烦,我贴上后逐句解释。
以秒针为例,DOM结构中,增加类属性的定义
<line class="pointer second" id="second" x1="400" y1="300" x2="400" y2="120"/>

CSS样式中仅保留second动画属性的定义
.second{animation:second linear 60s infinite}

但动画规则@keyframes通过以下方式实现:

  • javascript创建一个新的CSS样式标签,<style type="text/css">……</style>
  • 把规则作为内容写入新的样式标签中
  • 把新的样式标签追加进去
//创建新的CSS style标签
var style = document.createElement("style");
style.type = "text/css";
//创建可以接受变量参数的动画规则 
//初始关键帧为分针对应的角度
//结束关键帧为分针对应的角度
var keyFrames = "@keyframes second {\
  0% {transform: rotate("+secondDeg+"deg);}\
  100%{transform:rotate("+(secondDeg+360)+"deg)}}";
//把动画规则的内容写入新创建的style标签中
style.innerHTML = keyFrames;
//把新的style标签作为svg的子元素追加进去
document.getElementsByTagName("svg")[0].appendChild(style);

现在,只观察秒针,看看这种方法是不是可行。 f)

success!
通过这种方法,成功的把动态动画规则写入了CSS样式中。秒针的没有问题,那么分针和时针就是水到渠成的问题了。这里为了简化代码,封装了一个函数,把 动画规则的名称初始的度数 作为该函数的参数。

//创建一个新的CSS style标签
var style = document.createElement("style");
style.type = "text/css";
//定义一个新的style标签内容的函数,动画规则名称和初始度数作为参数
function newKeyFrames(name,degree){
var keyFrames = "@keyframes "+name+" {\
  0% {transform: rotate("+degree+"deg);}\
  100%{transform:rotate("+(degree+360)+"deg)}}";
//因为是新的style标签内容中不断增加新定义的动画规则,所以改成+=
style.innerHTML += keyFrames;
}

//时针的动画规则名称和初始旋转度数作为参数传入
newKeyFrames("hour",hourDeg);
//分针的动画规则名称和初始旋转度数作为参数传入
newKeyFrames("minute",minuteDeg);
//秒针的动画规则名称和初始旋转度数作为参数传入
newKeyFrames("second",secondDeg);
//把全部增加动画规则后的style样式标签追加到svg元素中
document.getElementsByTagName("svg")[0].appendChild(style);

CSS部分,需要分别增加动画属性的类的定义。

/*秒针每旋转360度,需要60秒*/
.second{animation:second linear 60s  infinite;}
/*分针每旋转360度,需要3600秒*/
.minute{animation:minute linear 3600s  infinite;}
/*时针每旋转360度,需要216000秒*/
.hour{animation:hour linear 216000s  infinite;}

DOM结构中记得把类样式附加上。

<line class="pointer second" id="second" />
<line class="pointer minute" id="minute" />
<line class="pointer hour" id="hour" />

可以看一下最终的效果了

似乎还有哪里有点问题,秒针的跳帧效果!

/*把线性linear去掉,改成steps(60) 即一个动画周期60秒跳帧为60*/
.second{animation:second steps(60) 60s  infinite;}

无懈可击!codepen附上源码base template of SVG+javascript+CSS3 clock

显而易见的是,第二种方法看上去并没有第一种那么简单,甚至可以说比较绕,那么为什么这里用了很多篇幅来探索这种方法的可行性?是因为这次的案例刚好是个时钟,适合用定时器而已,而大多数情况下,我们需要一种可接受变量作为动画规则参数的方法来解决问题,so。

5.以上是骨骼,以下才是灵魂

最难的部分已经完成,既然动画效果出来了,那么在此模板的基础上,可以做一些绚烂的效果了。(友情提示,建议在下面这些动效源码的基础上进行修改,有详细注释,可以直接用图层元素去替换)至于在哪个模板的基础上修改?全凭个人喜好,因为这个定时器是单线程的,不会吃内存,所以在这个案例中,暂时在这个模板的基础上替换。

<!DOCTYPE html>
<!--重点注意:!!!!Ai导出之前一定要把指针都放置初始零点的位置-->
<html lang="en">
<svg>
<style type="text/css">
/* Ai导出时生成的样式 每次使用时需要替换*/
.st0{  }
……
/* 旋转原点的值根据不同的底图需要重新定义 */
.pointer{transform-origin: }
</style>
<!--在进行图层排序时,遵循底图-时针-分针-秒针-覆盖层(如果有的话)的规律-->
<g id="base">
	底图对应的路径
</g>
<g class="pointer" id="hour">
时针对应的路径
</g>
<g  class="pointer" id="minute">
	分针对应的路径	
</g>
<g class="pointer" id="second">
	秒针对应的路径
</g>
<g id="overlay">
    覆盖层对应的路径
</g>
</svg>
<script>
// 因为没有用jQuery,因此封装了一个最基本的函数
    function $(id){
    var idValue=document.getElementById(id);
    return idValue;
}
</script>
<script>
//定义获取当前时间转换成旋转变形角度的函数
function timeId(){
var dt=new Date();
//时针度数修正,增加分针对应的度数
var hourDeg=dt.getHours()*30 + dt.getMinutes()*0.5;
//分针度数修正,增加秒针对应的度数
var minuteDeg=dt.getMinutes()*6+dt.getSeconds()*0.1;
var secondDeg=dt.getSeconds()*6;
//给时针追加旋转变形类样式
$("hour").style.cssText="transform:rotate("+hourDeg+"deg);";
//给分针追加旋转变形类样式
$("minute").style.cssText="transform:rotate("+minuteDeg+"deg);";
//给秒针追加旋转变形类样式
$("second").style.cssText="transform:rotate("+secondDeg+"deg);";
}
// 定时器触发前先执行一次函数
timeId();
// 设置每1000ms触发一次的定时器
setInterval(timeId, 1000);
</script>
</html>

喜欢clock?ok!比如这种不带刻度的简易风 codepen链接

以下均为各种无聊的炫技系列,懒得看可直接略过,无妨无妨。

光秃秃的底盘太单调?来个简易刻度怎样?codepen链接

不怕麻烦的全数字刻度 codepen链接

喜欢手表,说换就换,不就是替换一下的事情嘛codepen链接

太老土?那看看这种iWatch风格是不是你的菜

好了,不玩儿了,万变不离其宗。快过年了,最后来个中洋结合的,招财猫配金玉满堂金灿灿大福字的时钟。(我知道是猪年,但是招财猪,那个猪蹄子挥起来太有喜感,so)
关于招财猫的猫爪前后摆动可以参考我以前的一篇专栏,《无立体,不动画,CSS3 3D 动画属性入门》很基础的3D属性而已。只是因为是平面的,所以那个手臂的摆动会有些怪怪的。

祝各位新年鸿运当头,即使互联网寒冬再冷,也要笑着走下去。就酱。