学习用 JS/CSS 画一个时钟

看到某君的时钟 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 一下然后返回元素的数组即可。

计算刻针形状

时针目前的需求是:

  • 获取当前的最新时间刷新
  • 刷新频率为一秒钟。每一秒钟移动三种刻针,产生的角度均不同,当然最明显发生移动变化的是秒针,一秒钟移动一次;分针也在小幅移动,不过不明显
  • 三种刻针长度不一,也就是半径不一

下面结合我们的 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>
©️2020 CSDN 皮肤主题: 岁月 设计师:pinMode 返回首页