看到某君的时钟 Clock 代码,想学习怎么画一个时钟,逐重构之,把里面不合理的地方改进(例如用 js 写 css,那肯定不好)。
全部代码如下:
<html>
<head>
<meta charset="utf-8" />
<title>js时钟</title>
<style type="text/css">
#clock {
height: 300px;
width: 300px;
border: 1px solid #ccc;
position: relative;
}
.point {
line-height: 1px;
background-color: #000;
font-size: 1px;
position: absolute;
height: 3px;
width: 3px;
}
.hourone {
height: 8px;
width: 8px;
}
.minuteone {
height: 6px;
width: 6px;
background-color: #888;
}
.secondone {
background-color: #f00;
}
</style>
</head>
<body>
<div id="clock"></div>
<script>
var fn_sin = (angle, raduis) => Math.round(Math.sin(angle * Math.PI / 180) * raduis);
var fn_cos = (angle, raduis) => Math.round(Math.cos(angle * Math.PI / 180) * raduis);
var calhour = (h, m) => ((h * 60 + m) / (12 * 60)) * 360 - 90;
var calaa = (m) => (m / 60) * 360 - 90;
function Clock(box) {
var bh = box.clientHeight, bw = box.clientWidth, centerX = bw / 2, centerY = bh / 2;
this.raduis = (centerY >= centerX ? centerY : centerX) - 10;
this.centerX = centerX, this.centerY = centerY;
this.box = box;
this.createPoint();
var hour = this.createEl(30, 'hourone'),
minute = this.createEl(30, 'minuteone'),
second = this.createEl(40, 'secondone');
var update = (function () {
var d = new Date();
var h = Math.abs(d.getHours()), m = d.getUTCMinutes(), s = d.getSeconds();
this.set(hour, calhour(h, m), 0.5);
this.set(minute, calaa(m), 0.6);
this.set(second, calaa(s), 0.8);
}).bind(this);
update();
this.timer = setInterval(update, 1000);
}
Clock.prototype = {
createPoint() {
var _el = document.createElement('span');
_el.className = "point";
for (var angle = 0; angle < 360; angle += 30) {
var el = _el.cloneNode(true);
console.log(fn_cos(angle, this.raduis) ) // 角度 换算 弧度
el.style.left = this.centerX + fn_cos(angle, this.raduis) + 'px';
el.style.top = this.centerY + fn_sin(angle, this.raduis) + 'px';
this.box.appendChild(el);
}
},
createEl(amount, clsName) {
var arr = [], _el = document.createElement('span');
_el.className = "point " + clsName;
for (var i = 0; i < amount; i++) {
var el = _el.cloneNode(true);
this.box.appendChild(el);
arr.push(el);
}
return arr;
},
set(arr, angle, offset) {
for (var i = 0, len = arr.length; i < len; i++) {
var raduis = (i / len) * (this.raduis * offset);
arr[i].style.left = this.centerX + fn_cos(angle, raduis) + "px";
arr[i].style.top = this.centerY + fn_sin(angle, raduis) + "px";
}
}
};
new Clock(document.getElementById("clock"));
</script>
</body>
</html>
初始化变量
首先读者要懂得 CSS relative/absoulte 布局:容器定义了为 relative 布局后,自建一个内部的坐标系,我们就可以在里面把 absulte 的元素通过定义 top/left 分配坐标。
然后看看 Clock 这个类。构造参数 box 是个元素,读取其 clientHeight/clientWidth 获取高宽,分配给 bh、bw。bh/2、bw/2 自然就是中间的坐标,即变量 centerX、Y。我们还要将变量分配给当前对象一份引用,以便后面的方法读取它们,即 this.centerX = centerX, this.centerY = centerY;, box 也是如此。raduis 是半径,我们也分配给 this,即 this.raduis = (centerY >= centerX ? centerY : centerX) - 10; 减去 10 是我们希望这个时钟范围小一点,相当于内边距 padding。
绘制12刻度
目标是把12个点平均分配在圆中,如下图所示。
createPoint() 绘制了这十二个点,如下所示。
createPoint() {
var _el = document.createElement('span');
_el.className = "point";
for (var angle = 0; angle < 360; angle += 30) {
var el = _el.cloneNode(true);
console.log(fn_cos(angle, this.raduis) ) // 角度 换算 弧度
el.style.left = this.centerX + fn_cos(angle, this.raduis) + 'px';
el.style.top = this.centerY + fn_sin(angle, this.raduis) + 'px';
this.box.appendChild(el);
}
},
createPoint() 中,首先创建 span 元素,这元素是 absolute 定位,通过 left/top 可指定元素坐标。怎么确定坐标呢?首先一个圆有 360 度,被刻画为 12 点,即平均划分十二份,点与点之间的夹角是 360/12 = 30 度。于是,我们写出一个 for 循环,从零度开始到 360 度,步进是 30。然后,复制 12 个点,即 _el.cloneNode(true);
因为已知半径和夹角角度,所以可以通过三角函数 sin/cos 即可求得对边和邻边长度,对应就是 x、y 的距离。不过 JS 的 Math.sin/cos 函数要求传入的参数是弧度,所以我们要转换一下角度到弧度。最后因为圆心坐标是 centerX、centerY 所以最终刻点坐标还是加起来。
var fn_sin = (angle, raduis) => Math.round(Math.sin(angle * Math.PI / 180) * raduis);
var fn_cos = (angle, raduis) => Math.round(Math.cos(angle * Math.PI / 180) * raduis);
Math.round 的作用是取整。
绘制刻针
绘制黑色的时针、灰色的分针和红色的秒针,结果如下图。
虽然看上去是直线,但是仔细看还是有不少锯齿的,这是因为用一个个点构成直线的缘故,遇到有一定的角度如果点数量不够多的话,就容易出现锯齿。这里,一个点就是一个 span 元素,当然也是通过 left/top 定位。
首先是画出刻针 createEl(amount, clsName),指定 span 数量和样式类即可。
var hour = this.createEl(30, 'hourone'),
minute = this.createEl(30, 'minuteone'),
second = this.createEl(40, 'secondone');
……
createEl(amount, clsName) {
var arr = [], _el = document.createElement('span');
_el.className = "point " + clsName;
for (var i = 0; i < amount; i++) {
var el = _el.cloneNode(true);
this.box.appendChild(el);
arr.push(el);
}
return arr;
},
可见 createEl() 过程比较简单的,只是 for 一下然后返回元素的数组即可。
![](/qrcode.jpg)
计算刻针形状
时针目前的需求是:
- 获取当前的最新时间刷新
- 刷新频率为一秒钟。每一秒钟移动三种刻针,产生的角度均不同,当然最明显发生移动变化的是秒针,一秒钟移动一次;分针也在小幅移动,不过不明显
- 三种刻针长度不一,也就是半径不一
下面结合我们的 JS 程序,解决思路如下。
- 返回最新时间,包括时分秒,简单:
var d = new Date();
var h = Math.abs(d.getHours()), m = d.getUTCMinutes(), s = d.getSeconds();
- 一秒刷新一次,也就是定时器 setInterval,简单;求出移动角度的话,通过下面函数。
var calhour = (h, m) => ((h * 60 + m) / (12 * 60)) * 360 - 90; // 时针
var calaa = (m) => (m / 60) * 360 - 90; // 分针、秒针
相比之下,求出分针的角度比较简单的,我们先说一下(即 calaa())。一小时 60 分钟,表示我们在圆上等分 60 份,即有了 m 分钟即 m * (360/60) 角度。又因刻针相对 12 点的余角的角度,故减去 90 度,得出分针之角度。一分钟等于 60 秒,于是秒针亦是同理。
相比之下,求出时针较复杂。十二个小时,圆被等分 360/60,这个没问题。然后指针的角度产生的顶点不一定是落在 1、2、3 整数上,例如一点半(1:30),指针应指向 1 点与 2 点之间的中间位置上。于是 calhour 函数除 hour 参数外还需要 min 分的参数。转化为小数 (60 h + m)/60 再乘以 360/60,最后仍要减去 90,便最后得出时分的角度。注意 calhour 括号优先级不同,不影响结果。
角度确定了,我们接着就要在顶点与圆心之间画一条直线。之前我们不是弄了点的数组么,我们此时此刻就要将它们排列成一条线。我们令其半径不一样(角度是一样的了),所以坐标也不一样。怎么计算半径呢?与数组里面的索引 i 成比例,另外还加入一定偏移量令其更好看一点。如下 set 函数所示。
set(arr, angle, offset) {
for (var i = 0, len = arr.length; i < len; i++) {
var raduis = i / len * this.raduis * offset;
arr[i].style.left = this.centerX + fn_cos(angle, raduis) + "px";
arr[i].style.top = this.centerY + fn_sin(angle, raduis) + "px";
}
}
最后的 left/top 计算就是复用前面的三角函数。
最后的最后
至此一个简单的时钟完成了。用 span 元素绘图效率还是比较低,可以考虑用 canvas 或 svg 元素改进吧!
2019-8-23 Update: 基于 canvcas 制作的,可见行数很短。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HTML5 Canvas 圆形时钟动画</title>
<style type="text/css">
#myCanvas {
display: block;
margin: 10px auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="400" height="400"></canvas>
<script type="text/javascript">
var myCanvas = document.getElementById('myCanvas');
var c = myCanvas.getContext('2d');
function clock() {
c.clearRect(0, 0, 400, 400);
var data = new Date();
var sec = data.getSeconds();
var min = data.getMinutes();
var hour = data.getHours();
c.save();
c.translate(200, 200);
c.rotate(-Math.PI / 2);
//分钟刻度线
for (var i = 0; i < 60; i++) { //画12个刻度线
c.beginPath();
c.strokeStyle = "#f00";
c.lineWidth = 5;
c.moveTo(117, 0);
c.lineTo(120, 0);
c.stroke();
c.rotate(Math.PI / 30); //每个6deg画一个时钟刻度线
c.closePath();
}
//时钟刻度线
for (var i = 0; i < 12; i++) { //画12个刻度线
c.beginPath();
c.strokeStyle = "#000";
c.lineWidth = 8;
c.moveTo(100, 0);
c.lineTo(120, 0);
c.stroke();
c.rotate(Math.PI / 6); //每个30deg画一个时钟刻度线
c.closePath();
}
//外表盘
c.beginPath();
c.strokeStyle = "pink";
c.arc(0, 0, 145, 0, Math.PI * 2);
c.lineWidth = 12;
c.stroke();
c.closePath();
//画时针
hour = hour > 12 ? hour - 12 : hour;
//console.log(hour);
c.beginPath();
c.save();
c.rotate(Math.PI / 6 * hour + Math.PI / 6 * min / 60 + Math.PI / 6 * sec / 3600);
c.strokeStyle = "yellowgreen";
c.lineWidth = 4;
c.moveTo(-20, 0);
c.lineTo(50, 0);
c.stroke();
c.restore();
c.closePath();
//画分针
//console.log(min);
c.beginPath();
c.save();
c.rotate(Math.PI / 30 * min + Math.PI / 30 * sec / 60);
c.strokeStyle = "springgreen";
c.lineWidth = 3;
c.moveTo(-30, 0);
c.lineTo(70, 0);
c.stroke();
c.restore();
c.closePath();
//画秒针
c.beginPath();
c.save();
c.rotate(Math.PI / 30 * sec);
c.strokeStyle = "red";
c.lineWidth = 2;
c.moveTo(-40, 0);
c.lineTo(120, 0);
c.stroke();
c.restore();
c.closePath();
c.restore();
}
clock();
setInterval(clock, 1000);
</script>
</body>
</html>