整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

时钟和倒计时:CSS 和 JavaScript 中的计时


一天,我需要一个数字时钟组件,所以我很快编写了一个简单的 JavaScript 方法:

function uiDigitalClock(node) {
  const now = () => {
    node.textContent = new Date().toLocaleTimeString()
    requestAnimationFrame(now)
  }
  now()
}

我在某处读到,这requestAnimationFrame应该比 更好的性能setInterval,但该方法一直困扰着我。

在最佳条件下,每秒requestAnimationFrame触发60 次——这比数字时钟需要的多 60 倍!

Date()每秒创建 60 次对象对性能来说并不好!

所以我尝试了setInterval:

function interval(node){
  return setInterval(() => node.textContent = new Date().toLocaleTimeString(), 1000)
}

同样,代码不多,视觉结果是相同的 - 并且每秒只有一个对象。 Date()

又怎样呢setTimeout()?

function timeout(node) {
  const now = () => {
    node.textContent = new Date().toLocaleTimeString()
    setTimeout(now, 1000)
  }
  now()
}

它也能得到类似的结果,但有令人讨厌的延迟(setImmediate()仅在 Node 中)

然后我决定修改初始版本,只每秒触发一次,并requestAnimationFrame()返回时间戳:

function frame(node) {
  let last = 0;
  const render = now => {
    if (!last || now - last >= 1000) {
      last = now;
      node.textContent = new Date().toLocaleTimeString();
    }
    requestAnimationFrame(render);
  }
  window.requestAnimationFrame(render);
}

当所有 3 个方法都运行时,我碰巧检查了开发工具中的标记,并注意到我什至无法扩展节点,因为它们不断刷新,并且看起来像旧的<blink>-tag!


向巴德寻求帮助

仍然不知道该选择哪种方法,我决定询问 Google Bard,它回答说:

  • Clock1使用该setInterval()方法创建一个每 1000 毫秒(1 秒)触发一次的循环计时器。这是性能最低的方法,因为它为每次迭代创建一个新的计时器,这可能会增加大量的开销。
  • Clock2使用该setTimeout()方法,创建一个在 1000 毫秒后触发的计时器。这比clock1 性能更高,因为它不会为每次迭代创建新的计时器。然而,它的性能仍然不如clock3,因为浏览器必须等待计时器触发才能更新文本内容。
  • Clock3使用该requestAnimationFrame()方法,这是请求浏览器重绘的更有效方法。浏览器只会在必要时重新绘制元素,这可以节省大量的 CPU 时间。

好吧,我决定使用clock3 ...但后来我突然想到:“ CSS 中的数字动画怎么样?使用@property?


Jhey不久前制作了一个纯 CSS 秒表,所以我决定尝试类似的东西。

@property --seconds {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
@property --minutes {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
@property --hours {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}

然后,在<ol>-tag 中,我<li>为每个时间单位添加了一个 -tag。

要使用 - 声明的值@property,您需要使用 CSS 计数器,因此几秒钟内它是:

.seconds {
  animation: seconds 60s steps(60, end) infinite;
  animation-delay: var(--delay-seconds, 0s);
  counter-reset: seconds var(--seconds);
  &::after { content: counter(seconds, decimal-leading-zero) ' '; }
}

要为秒设置动画,需要一个关键帧:

@keyframes seconds { 
  from { --seconds: 0;}
  to { --seconds: 60; }
}

对于几分钟来说,几乎是一样的,但是动画花费了 60 倍的时间 (60 x 60 = 3600):

animation: minutes 3600s steps(60, end) infinite;

对于几个小时,我们需要将该数字乘以 24:

animation: hours 86400s steps(24, end) infinite;

耶!我们有一个可以工作的 CSS 时钟……但它只在午夜工作,因为小时、分钟和秒都从0(零)开始。

那么该怎么办?创建初始对象后,我可以轻松地从 JavaScript 更新属性Date()。

但这样动画就会出错,因为它们会运行相同的时间(每秒钟 60 秒),即使实际的秒数小于该值。

我在 Twitter 上寻求帮助——幸运的是,Temani Afif 和 Álvaro Montoro 回复了!解决方案是使用负数 animation-delay。

因此,使用一些 JavaScript 来设置当前时间并计算延迟:

const time = new Date();
const hours = time.getHours();
const minutes = time.getMinutes();
const seconds = time.getSeconds();

// Delays
const HOURS = -Math.abs((hours * 3600) + (minutes * 60) + seconds);
const MINS = -Math.abs((minutes * 60) + seconds);
const SECS = -Math.abs(seconds);

...我们可以更新之前指定的 CSS 属性,例如:

node.style.setProperty(`--delay-seconds`, `${seconds}s`);

现在,我们有了一个可以工作的数字 CSS 时钟——将其与此处的其他方法进行比较:


如果您在开发工具中检查标记,您会发现 CSS 版本并未重写 DOM 内容。


倒数

之后,我决定重新审视我的旧 Codepen,多语言倒计时,并制作一个纯 CSS 版本:


locale如果您想要其他语言,您可以在 JS 代码中使用:


但性能呢?CSS 可能不会像 JavaScript 那样阻塞主线程,但我们能确定它使用 GPU 而不是 CPU 吗?

有一个老技巧:

.useGpu {
  transform: translateZ(0);
  will-change: transform;
}

然后,在开发工具中,转到“图层”:


看到“倒计时”现在如何拥有自己渲染层了吗?不确定这是否仍然适用,但我猜添加也没什么坏处。


离开浏览器选项卡

当我离开浏览器选项卡并返回时,纯 CSS 时钟没有出现任何问题。也许是我等的时间还不够长吧!但如果您遇到任何问题,请使用此事件重新计算时钟的延迟:

document.addEventListener('visibilitychange', () => {
  if (!document.hidden) { ... }
})

模拟时钟

作为奖励 - 这是一个模拟时钟,我不久前做了:

要自学前端开发,你要的学习资料到了-前端/JAVA/PHP学习交流群,新版css时钟效果图

<!DOCTYPE html>

<html>

<head>

<metahttp-equiv="Content-Type" content="text/html;charset=UTF-8">

<title>RunJS</title>

<style>

.clock{

width:200px;

height:200px;

border-radius:100%;

position:relative;

background-image:url(

);

background-size:100%;

}

.line{

height:4px;

background-color:red;

margin-left:-15px;

margin-top:-2px;

}

.original{

position:absolute;

left:50%;

top:50%;

width:1px;

height:1px;

float:left;

}

.clock>.point{

position:absolute;

top:50%;

left:50%;

margin-left:-5px;

margin-top:-6px;

width:3px;

height:3px;

padding:5px;

background-color:red;

border-radius:13px;

}

.original.seconds{

-webkit-animation:rotate_origin60s linear infinite;

animation:rotate_origin60s linear infinite;

}

.original.seconds>.line{

background-color:red;

width:100px;

height:2px;

}

.original.minutes{

-webkit-animation:rotate_origin3600s linear infinite;

animation:rotate_origin3600s linear infinite;

}

.original.minutes>.line{

background-color:blue;

width:80px;

height:3px;

}

.original.hours{

-webkit-animation:rotate_origin86400s linear infinite;

animation:rotate_origin86400s linear infinite;

}

.original.hours>.line{

width:60px;

background-color:green;

}

@-webkit-keyframes rotate_origin{

from{

-webkit-transform:rotateZ(0deg);

}

to{

-webkit-transform:rotateZ(360deg);

}

}

@keyframes rotate_origin{

from{

transform:rotateZ(0deg);

}

to{

transform:rotateZ(360deg);

}

}

</style>

</head>

<body>

<divclass="clock">

<divclass="original hours">

<divclass="line"></div>

</div>

<divclass="original minutes">

<divclass="line"></div>

</div>

<divclass="original seconds">

<divclass="line"></div>

</div>

<divclass="point"></div>

</div>

</body>

</html>

不久用JS+html<canvas>标签实现了简易时钟(见HTML使用Canvas绘制动画时钟),最近学习C/C++语言(话说怎么区分写的是c还是c++?),恰好看到一个有趣的绘图库EasyX,拿它来练练手,就先做个简易时钟看看吧。

EasyX Graphics Library 是针对 Visual C++ 的免费绘图库,支持 VC6.0 ~ VC2022。EasyX可以帮助 C/C++ 初学者快速上手图形和游戏编程。

比如,可以基于 EasyX 图形库很快的用几何图形画一个房子,或者一辆移动的小车,可以编写俄罗斯方块、贪吃蛇、黑白棋等小游戏,可以练习图形学的各种算法,等等。

简单了解了EasyX后,就可以开干了。本文开发环境是windows 11和Visual Studio 2022。首先打开vs2022并创建一个空的控制台项目,在项目里新建main.cpp,#include EasyX的头文件graphics.h。提示:由于EasyX是针对C++的绘图库,必须用cpp文件来开发编译,但是编码可以是c也可以是c++。

EasyX使用非常简单,可以一边阅读EasyX在线文档(https://docs.easyx.cn/zh-cn/intro)一边开发。基本流程:

#include <graphics.h>		// 引用图形库头文件
int main()
{
	initgraph(600, 400);	// 创建绘图窗口,大小为 600x400 像素
	//绘图操作
	closegraph();			// 关闭绘图窗口
	return 0;
}

下面基于以上基础代码逐步添加。

准备工作

由于EasyX默认情况下生成的绘图界面是黑色的背景,我们可以使用setbkcolor函数更改界面背景色为白色:

setbkcolor(WHITE);
cleardevice(); //设置背景色后要清除设备才能生效
setlinecolor(BLACK);//设置线条颜色(白底黑线)

在 EasyX 中,坐标分两种:物理坐标和逻辑坐标。

物理坐标是描述设备的坐标体系。

坐标原点在设备的左上角,X 轴向右为正,Y 轴向下为正,度量单位是像素。

坐标原点、坐标轴方向、缩放比例都不能改变。

逻辑坐标是在程序中用于绘图的坐标体系。

坐标默认的原点在窗口的左上角,X 轴向右为正,Y 轴向下为正,度量单位是点。

默认情况下,逻辑坐标与物理坐标是一一对应的,一个逻辑点等于一个物理像素。

默认原点(0,0)在绘图窗口的左上角,示例中为了方便绘制时钟,将原点修改为窗口的中心点:

//假设窗口大小为600x400,则中心点为(600/2,400/2)
setorigin(300,200);
//坐标原点设置成功后,(300,200)位置即为绘图坐标的原点(0,0)

为方便代码编写,先定义了几个常量:

#define W 800	//窗口宽度
#define H 620	//窗口高度
#define oY H/2	//调整坐标原点y为窗口高度的一半
#define R 290	//时钟内圆半径
#define PI 3.14
#define HD PI/180 //因需多次计算弧度,提前算出部分公共值
#define R_2 R/2

绘制钟面内外框

时钟可以分成钟面外框、大小刻度和指针。由于指针指向是需要随时间动态变化,因此不能固定不变。而钟面外框是固定不变的,可以只绘制一次。(大小刻度也是固定不变的,但是由于与秒针行走路径有重叠,也要动态绘制)

示例中钟面外框绘制成两个大圆,都有一定的宽度。

绘制圆形的函数是void circle( int x, int y, int radius );参数分别是圆心的x、y坐标以及半径长度,如果要绘制宽度,可以使用setlinestyle()函数设置线条的形状和宽度:

setlinecolor(BLACK);//设置线条颜色
setlinestyle(PS_SOLID, 10);//设置线条样式和宽度
circle(0, 0, oY - 10); //绘制时钟外黑框
//画出时钟的内框
setlinestyle(PS_SOLID, 4);
circle(0, 0, R);
setlinestyle(PS_SOLID, 1);//绘制完钟面内外两个框后恢复线条宽度为1

绘制钟面内外两个框

绘制大小刻度和12个数字

由于大小刻度与指针有部分重叠,因此也要跟随指针一起反复动态绘制。因此都将它们放入一个while循环中。

示例中,大刻度画小实心圆,小刻度画短线条。画刻度和指针都需要定位坐标,需要用上数学计算,须先引入头文件math.h。具体的坐标计算方法可以自行度娘或看前一篇js+canvas绘制时钟,里面有简单的解释。

setfillcolor(BLACK);//大刻度实心小圆的填充色(黑色)
double _x, _y;
TCHAR s[3];
settextcolor(BLACK);
LOGFONT f;
gettextstyle(&f);
f.lfHeight = 36; //设置字体大小
_tcscpy_s(f.lfFaceName, _T("serif")); //设置字体名称
f.lfQuality = ANTIALIASED_QUALITY;	//设置字体平滑效果
settextstyle(&f);

for (int j = 9, i = 0; i < 12; j++, i++) {//由于坐标0度指向刻度3,所以有针对性地修正一下
	if (i > 2) { j = i - 3; }
	int _t = j * 30;//每个大格30度,用于下面的弧度计算
	_x = cos(HD * _t) * (R - 5);
	_y = sin(HD * _t) * (R - 5);//计算大刻度的圆心坐标

	fillcircle(_x, _y, 5);//绘制大刻度
//开始绘制文字,先计算文字显示位置的矩形坐标
	swprintf_s(s, _T("%d"), i == 0 ? 12 : i);
	RECT tr;//定义文字矩形结构
	if (j == 10) {//修正部分矩形形状,j==10指向1时
		tr.left = _x - 50;
		tr.top = _y;
		tr.right = _x;
		tr.bottom = _y + 50;
	}else if(j==4){//指向7时
		tr.left = _x +50;
		tr.top = _y;
		tr.right = _x;
		tr.bottom = _y -50;
		
	}else if (j == 1) {//指向4时
		tr.left = _x - 50;
		tr.top = _y - 50;
		tr.right = _x;
		tr.bottom = sin(HD * (5+_t)) * (R - 5);
	}else if( j == 7) {//指向10时
		tr.left = _x+10;
		tr.top = sin(HD * (5 + _t)) * (R - 5);
		tr.right = _x+50;
		tr.bottom = _y+50;
	}else {
		tr.left = cos(HD * (_t - 5 < 0 ? 355 : _t - 5)) * (R);
		tr.top = sin(HD * (_t - 5 < 0 ? 355 : _t - 5)) * (R);
		tr.right = cos(HD * (_t + 5)) * (R - 70);
		tr.bottom = sin(HD * (_t + 5)) * (R - 70);
	}
	//简单地绘制12个数字,如果有更好的定位方式欢迎告知,谢谢。
	drawtext(s, &tr, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//绘制60个小刻度
setlinestyle(PS_SOLID | PS_ENDCAP_SQUARE, 3);
double xx = 0;
for (int j = 0, i = 0; i < 60; i++) {
	j = i - 15;
	if (j < 0) { j = 60 + j; }
	if (j % 5 == 0) { continue; }
	xx = HD * j * 6;
	line(cos(xx) * (R - 10), sin(xx) * (R - 10), cos(xx) * (R - 5), sin(xx) * (R - 5));
}
//绘制圆心
fillcircle(0, 0, 8);

效果图:

绘制大小刻度和圆心

绘制指针

指针有三种,时针粗短,分针适中,秒针细长,示例中将秒针绘制成红色以示区别。指针的位置是根据时间动态更新的,所以先要获取当前的系统时间,可以简单地引入time.h头文件。

//绘制时针,参数为当前时,分,秒
void drawHp(int h,int m,int s) {
	h -= 3;
	if (h < 0) {
		h = 12 + h;
	}
	double hd = HD * h * 30 + PI/360 * m +PI/21600 * s;
	int _x = cos(hd) * (R_2+10);
	int _y = sin(hd) * (R_2+10);
	drawPt(_x, _y, 8);
}
//绘制分针,参数为当前分,秒
void drawMp(int h,int s) {
	h -= 15;
	if (h < 0) {
		h = 60+h;
	}
	double hd = HD * (double)(h + s / static_cast<double>(60)) * 6;
	int _x = cos(hd) * (R_2+40);
	int _y = sin(hd) * (R_2+40);
	drawPt(_x, _y, 5);
}
//绘制秒针,参数为当前秒
void drawSp(int h) {
	h -= 15;
	if (h < 0) {
		h = 60 + h;
	}
	double hd = HD * h * 6;
	int _x = cos(hd) * (R_2 + 120);
	int _y = sin(hd) * (R_2 + 120);
	
	drawPt(_x, _y, 2);
}
//根据指针x、y坐标真实绘制指针
void drawPt(int x, int y,int w) {
	if (w == 2) {
		setlinecolor(RED);
	}
	setlinestyle(PS_SOLID | PS_JOIN_ROUND | PS_ENDCAP_ROUND, w);
	line(0, 0, x, y);
	setlinecolor(BLACK);
}

调用方式:

time_t now = time(NULL);
struct tm info;
localtime_s(&info,&now);//获得本地时间
int hour = info.tm_hour;
int minute = info.tm_min;
int second = info.tm_sec;

drawHp(hour,minute,second);
drawMp(minute,second);
drawSp(second);

让我们看看效果如何。

绘制了指针的时钟,出现了好多的秒针轨迹

哈哈,闹笑话了。由于我们在实时地?计算新的指针坐标并更新绘制新的指针位置,因此界面上绘制出了很多的秒针轨迹。怎么解决呢?方法肯定是有的,可以绘制前先清除设备(调用cleardevice()函数,将清除整个绘图窗口),我们这里不打算清除全部,只把钟面圆框内的部分清除:

clearcircle(0, 0, oY-25);//先清除内圆框之内的全部
//然后再写动态绘图刻度和指针的代码

看看效果如何:

到此为止,一个简单的时钟已经绘制完成了。但是你可能发现有点小问题:界面有时会闪烁一下。这是由于动态更新指针位置并实时绘制出来造成的闪烁,可以使得经典的双缓冲技术来解决。EasyX很贴心地提供了这个技术,只需要三个函数:

BeginBatchDraw();
while(true){
  //反复的绘制操作
	FlushBatchDraw();
}
EndBatchDraw();

就是这么简单。在我们的示例代码中加上它们就行了。

简单优化:节流

示例中将动态时间计算和实时更新绘图都放在了while循环中,计算机超强的计算速度下,每秒可将会计算非常多次,示例中时钟是按秒走的,我们可以采用节流思想,在一段时间的指定时间间隔内只执行一次回调函数,限制在一定时间内的执行次数,从而控制高频率触发的事件,避免过多的计算或操作影响性能。最简单的做法是一次绘图更新完成后,sleep一定时间。也可以根据系统时钟打点数计算fps:

const clock_t FPS = 1000 / 2;//每秒只执行两次
clock_t startTime, freamTime;
while (true) {
	//计算帧率
		startTime = clock();
		freamTime = clock() - startTime;

		if (freamTime < FPS)
		{
			Sleep(FPS - freamTime);
		}
  //将各种动态绘图代码写在此处
}

完整代码(Visual stdio 2022 编译通过)