服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

node.js|vue.js|jquery|angularjs|React|json|js教程|

服务器之家 - 编程语言 - JavaScript - js教程 - Web Animations API实现一个精确计时的时钟示例

Web Animations API实现一个精确计时的时钟示例

2022-07-30 15:31前端修罗场 js教程

这篇文章主要为大家介绍了Web Animations API实现一个精确计时的时钟示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

正文

在 JavaScript 中,当事情准时发生时,很自然地会想到使用计时器函数。 但是,当某件事由于其他事情依赖于它而在准确的时刻发生时,你很快就会发现计时器会存在一个不准时的问题。而本文所要介绍的 Web Animations API 可以在某些情况下替代计时器函数,同时保持精确。

当你需要处理精确的视觉呈现时,你就会发现你花费了太多时间来解决 JavaScript 无法准确解决代码何时实际执行的问题。

例如,下面就举了一个计时器准确性的问题。

JavaScript 计时器问题

在 JavaScript 中,每个任务都会经过一个队列。 包括你的代码、用户交互、网络事件等都会放入各自的任务队列,进行事件循环处理。 这么做能够保证任务按顺序发生。例如,当事件触发或计时器到期时,你在回调中定义的任务将进入到队列。 一旦事件循环轮到了它,你的代码就会被执行。

可是,当在任务队列中执行计数器函数时,问题就会暴露了。

低精度

在将任务放入队列之前,我们可以准确定义超时应该等待多长时间。 但是,我们无法预测的是目前队列中会出现什么。这是因为 setTimeout 保证在将事物放入队列之前的最小延迟。 但是没有办法知道队列中已经有什么。

曾经我不得不为一个网站实现随机翻转图块,其中一个错误是由休眠标签引起的。 因为每个图块都有自己的计时器,所以当标签激活时,它们都会同时触发。那个案例如下代码所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<article id="demo">
  <section>
    <h3>Timeouts</h3>
    <div class="row">
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
    </div>
  </section>
  <section>
    <h3>Animations</h3>
    <div class="row">
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
      <div class="square">
        <div></div>
        <div></div>
      </div>
    </div>
  </section><button type="button">&#8227; Run</button>
</article>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#demo {
  display: flex;
  background-color: white;
  color: black;
  flex-flow: column nowrap;
  align-items: center;
  padding: 2rem;
  gap: 2rem;
}
.row {
    display: flex;
    gap: 0.5rem;
}
.square {
  display: flex;
  width: 5rem;
  height: 5rem;
  position: relative;
  transform-style: preserve-3d;
}
.square > * {
  flex: 1 0 100%;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  background-color: green;
}
.square > *:last-child {
  background-color: rgb(227, 227, 0);
  position: absolute;
  width: 100%;
  height: 100%;
  transform: rotateY(0.5turn);
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(function () {
    "use strict";
    const flip_keyframe = {
        transform: [
            "rotateX(0turn)",
            "rotateX(0.5turn)",
        ]
    };
    const timing_options = {
        duration: 1000,
        fill: "forwards"
    }
    function create(element) {
        const animation = element.animate(flip_keyframe, timing_options);
        animation.pause();
        return animation;
    }
    function reset(animation) {
        animation.pause();
        animation.currentTime = 0;
    }
    const id = "demo";
    const demo = document.getElementById(id);
    const sections = demo.querySelectorAll("section");
    const first_row_animations = Array.from(
        sections[0].lastElementChild.children
    ).map(create);
    const second_row_animations = Array.from(
        sections[1].lastElementChild.children
    ).map(create);
    const button = document.querySelector("button");
    button.addEventListener("click", function (event) {
        const start_time = document.timeline.currentTime;
        first_row_animations.forEach(reset);
        second_row_animations.forEach(reset);
        first_row_animations.forEach(function (animation, index) {
            setTimeout(function () {
                animation.play();
            }, 250 * index);
        });
        second_row_animations.forEach(function (animation, index) {
            animation.startTime = start_time + (250 * index);
        });
        setTimeout(function () {
            const start = Date.now();
            while (Date.now() - start < 400) {}
        }, 500);
    });
}());

Web Animations API实现一个精确计时的时钟示例

为了解决这个问题,我想到了 Web Animations API。

Web Animations API

Web Animations API 引入了时间线的概念。 默认情况下,所有动画都与文档的时间轴相关联。 这意味着动画共享相同的“内部时钟”——即从页面加载开始的时钟。

共享时钟使我们能够协调动画。无论是某种节奏还是一种模式,你都不必担心某些事情会延迟或超前发生。

开始时间

要使动画在某个时刻开始,请使用 startTime 属性。 startTime 的值以页面加载后的毫秒数为单位。 开始时间设置为 1000.5 的动画将在文档时间轴的 currentTime 属性等于 1000.5 时开始播放。

你是否注意到开始时间值中的小数点了吗? 是的,你可以使用毫秒的分数来精确时间。 但是,精确度取决于浏览器设置。

另一个有趣的事情是开始时间也可以是负数。 你可以自由地将其设置为未来的某个时刻或过去的某个时刻。 将该值设置为 -1000,你的动画状态就像页面加载时已经播放了一秒钟一样。 对于用户来说,动画似乎在他们甚至还没有考虑访问你的页面之前就已经开始播放了。

下面我们给出一个示例一起来看下如何使用 Web Animations API。

示例:精确计时的时钟

这个例子是一个精确计时的时钟,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template id="tick">
    <div class="tick"><span></span></div>       
</template>
<template id="digit"><span class="digit" style="--len: 10;"><span></span></span></template>
<div id="analog-clock">
    <div class="hour-ticks"></div>
    <div class="minute-ticks"></div>
    <div class="day"></div>
    <div class="hand second"><div class="shadow"></div><div class="body"></div></div>
    <div class="hand minute"><div class="shadow"></div><div class="body"></div></div>
    <div class="hand hour"><div class="shadow"></div><div class="body"></div></div>
    <div class="dot"></div>
</div>
<div id="digital-clock">
    <span class="hours"></span><span>:</span><span class="minutes"></span><span>:</span><span class="seconds"></span><span>.</span><span class="milliseconds"></span>
</div>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
:root {
    --face-size: 15rem;
}
body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: sans-serif;
}
body > * {
    margin: 1rem;
}
#analog-clock {
    width: var(--face-size);
    height: var(--face-size);
    position: relative;
    border: 3px solid #555;
    border-radius: 50%;
    font-weight: 400;
}
.dot {
    --size: 9px;
    position: absolute;
    left: calc(50% - calc(var(--size) / 2));
    top: calc(50% - calc(var(--size) / 2));
    width: var(--size);
    height: var(--size);
    background-color: #333;
    border-radius: 50%;
    filter: drop-shadow(1px 1px 1px #333);
}
.hand {
    position: absolute;
    bottom: 50%;
    left: calc(50% - calc(var(--width) / 2));
    width: var(--width);
    transform-origin: center bottom;
}
.hand > * {
    position: absolute;
    height: 100%;
    width: 100%;
    border-radius: 4px;
}
.hand .body {
    background-color: #333;
}
.hand .shadow {
    background-color: black;
    opacity: 0.2;
    filter: drop-shadow(0 0 1px black);
}
.second {
    --width: 1px;
    height: 50%;
    transform-origin: center 80%;
    margin-bottom: calc(var(--face-size) * -0.1)
}
.second .body {
    background-color: black;
}
.minute {
    --width: 3px;
    height: 35%;
}
.hour {
    --width: 5px;
    height: 25%;
}
.day {
    --size: 2ch;
    position: absolute;
    left: calc(50% - calc(var(--size) / 2));
    top: calc(50% - calc(var(--size) / 2));
    width: var(--size);
    height: var(--size);
    transform: translate(calc(var(--face-size) * 0.2));
}
.tick {
    --width: 2px;
    --height: 29px;
    --shift: translateY(calc(var(--face-size) / -2));
    position: absolute;
    width: var(--width);
    height: var(--height);
    background-color: #666;
    top: 50%;
    left: calc(50% - calc(var(--width) / 2));
    transform-origin: top center;
}
.tick > span {
    --width: calc(calc(var(--face-size) * 3.141592653589793) / 24);
    position: absolute;
    width: var(--width);
    top: 3px;
    left: calc(var(--width) / -2);
    text-align: center;
}
.hour-ticks .tick:nth-child(even) > span {
    display: none;
}
.hour-ticks .tick:nth-child(odd) {
    background: none;
}
.hour-ticks .tick {
    transform: rotate(calc(var(--index) * 15deg)) var(--shift);
}
.minute-ticks .tick {
    --width: 1px;
    --height: 5px;
    --shift: translateY(calc(var(--face-size) / -2.5));
    background-color: black;
    transform: rotate(calc(var(--index) * 6deg)) var(--shift);
}
.minute-ticks .tick:nth-child(5n+1) {
    display: none;
}
#digital-clock {
    font-size: 1.5rem;
    line-height: 1;
}
#digital-clock > span {
    display: inline-block;
    vertical-align: top;
}
.digit {
    display: inline-block;
    overflow: hidden;
    max-width: 1ch;
}
.digit.wide {
    max-width: 2ch;
}
.digit > span {
    display: inline-flex;
    align-items: flex-start;
}
.digit.wide > span > span {
    min-width: 2ch;
    text-align: right;
}
.day .digit > span > span {
    text-align: center;
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
const ms = 1;
const s = ms * 1000;
const m = s * 60;
const h = m * 60;
const d = h * 24;
const start_time = (function () {
    const time = new Date();
    const document_time = document.timeline.currentTime;
    const hour_diff = time.getHours() - time.getUTCHours();
    const current_time = (Number(time) % d) + (hour_diff * h);
    return document_time - current_time;
}());
const single_digit_keyframes = [
    {transform: "translateX(0)"},
    {transform: "translateX(calc(var(--len, 10) * -1ch)"}
];
const double_digit_keyframes = [
    {transform: "translateX(0)"},
    {transform: "translateX(calc(var(--len) * -2ch)"}
];
function range(len) {
    return new Array(len).fill(true);
}
function digits(len = 10, zero_based = true) {
    const digit = document.getElementById("digit").content.cloneNode(true);
    digit.firstElementChild.style.setProperty("--len", len);
    digit.firstElementChild.firstElementChild.append(
        ...range(len).map(function (ignore, index) {
            const span = document.createElement("span");
            span.textContent = zero_based ? index : index + 1;
            return span;
        })
    );
    if (len > 10) {
        digit.firstElementChild.classList.add("wide");
    }
    return digit;
}
(function build_analog_clock() {
    const clock = document.getElementById("analog-clock");
    const tick_template = document.getElementById("tick");
    const hour_marks_container = clock.querySelector(".hour-ticks");
    const minute_marks_container = clock.querySelector(".minute-ticks");
    const day = clock.querySelector(".day");
    hour_marks_container.append(...range(24).map(function (ignore, index) {
        const tick = tick_template.content.cloneNode(true);
        const shifted = index + 1;
        tick.firstElementChild.style.setProperty("--index", shifted);
        tick.firstElementChild.firstElementChild.textContent = shifted;
        return tick;
    }));
    minute_marks_container.append(...range(60).map(function (ignore, index) {
        const tick = tick_template.content.cloneNode(true);
        tick.firstElementChild.style.setProperty("--index", index);
        tick.firstElementChild.firstElementChild.remove();
        return tick;
    }));
}());
(function build_digital_clock() {
    const clock = document.getElementById("digital-clock");
    const hours = clock.querySelector(".hours");
    const minutes = clock.querySelector(".minutes");
    const seconds = clock.querySelector(".seconds");
    const milliseconds = clock.querySelector(".milliseconds");
    hours.append(digits(24));
    minutes.append(digits(6), digits());
    seconds.append(digits(6), digits());
    milliseconds.append(digits(), digits(), digits());
}());
(function start_analog_clock() {
    const clock = document.getElementById("analog-clock");
    if (clock === null) {
        return;
    }
    const second = clock.querySelector(".second");
    const minute = clock.querySelector(".minute");
    const hour = clock.querySelector(".hour");
    const hands = [second, minute, hour];
    const hand_durations = [m, h, d];
    const steps = [60, 60, 120];
    const movement = [];
    hands.forEach(function (hand, index) {
        const duration = hand_durations[index];
        const easing = `steps(${steps[index]}, end)`;
        movement.push(hand.animate(
            [
                {transform: "rotate(0turn)"},
                {transform: "rotate(1turn)"}
            ],
            {duration, iterations: Infinity, easing}
        ));
        const shadow = hand.querySelector(".shadow");
        if (shadow) {
            movement.push(shadow.animate(
                [
                    {transform: "rotate(1turn) translate(3px) rotate(0turn)"},
                    {transform: "rotate(0turn) translate(3px) rotate(1turn)"}
                ],
                {duration, iterations: Infinity, iterationStart: 0.9, easing}
            ));
        }
    });
    movement.forEach(function (move) {
        move.startTime = start_time;
    });
}());
(function start_digital_clock() {
    const clock = document.getElementById("digital-clock");
    if (clock === null) {
        return;
    }
    const milliseconds = clock.querySelector(".milliseconds");
    const seconds = clock.querySelector(".seconds");
    const minutes = clock.querySelector(".minutes");
    const hours = clock.querySelector(".hours");
    const sections = [seconds, minutes];
    const durations = [s, m, h];
    const animations = [];
    Array.from(
        milliseconds.children
    ).reverse().forEach(function (digit, index) {
        animations.push(digit.firstElementChild.animate(
            single_digit_keyframes,
            {
                duration: ms * (10 ** (index + 1)),
                iterations: Infinity,
                easing: "steps(10, end)"
            }
        ));
    });
    sections.forEach(function (section, index) {
        Array.from(
            section.children
        ).forEach(function (digit) {
            const nr_digits = digit.firstElementChild.children.length;
            animations.push(digit.firstElementChild.animate(
                single_digit_keyframes,
                {
                    duration: (
                        nr_digits === 10
                        ? durations[index] * 10
                        : durations[index + 1]
                    ),
                    iterations: Infinity,
                    easing: `steps(${nr_digits}, end)`
                }
            ));
        });
    });
    Array.from(hours.children).forEach(function (digit) {
        const nr_digits = digit.firstElementChild.children.length;
        animations.push(
            digit.firstElementChild.animate(
                double_digit_keyframes,
                {
                    duration: d,
                    iterations: Infinity,
                    easing: `steps(${nr_digits}, end)`
                }
            )
        );
    });
    animations.forEach(function (animation) {
        animation.startTime = start_time;
    });
}());
(function set_up_date_complication() {
    const day = document.querySelector(".day");
    if (day === null) {
        return;
    }
    function month() {
        const now = new Date();
        return digits(
            (new Date(now.getFullYear(), now.getMonth() + 1, 0)).getDate(),
            false
        );
    }
    function create_animation(digit) {
        const nr_digits = digit.firstElementChild.children.length;
        const duration = d * nr_digits;
        return digit.firstElementChild.animate(
            double_digit_keyframes,
            {
                duration,
                easing: `steps(${nr_digits}, end)`,
                iterationStart: (d * ((new Date()).getDate() - 1)) / duration
            }
        );
    }
    const new_day = day.cloneNode();
    new_day.append(month());
    day.replaceWith(new_day);
    Array.from(new_day.children).forEach(function (digit) {
        const complication = create_animation(digit);
        complication.startTime = start_time;
        complication.finished.then(set_up_date_complication);
    });
}());

效果如下:

Web Animations API实现一个精确计时的时钟示例

因为时钟是一种精密仪器,所以我让秒针和分针在它们对应的值发生变化的那一刻改变它们的位置。 下面的代码说明了如何进行精确计时:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const clock = document.getElementById("analog-clock");
const second = clock.querySelector(".second");
const minute = clock.querySelector(".minute");
const hour = clock.querySelector(".hour");
const s = 1000;
const m = s * 60;
const h = m * 60;
const d = h * 24;
const hands = [second, minute, hour];
const hand_durations = [m, h, d];
const steps = [60, 60, 120];
const movement = hands.map(function (hand, index) {
    return hand.animate(
        [
            {transform: "rotate(0turn)"},
            {transform: "rotate(1turn)"}
        ],
        {
            duration: hand_durations[index],
            iterations: Infinity,
            easing: `steps(${steps[index]}, end)`
        }
    );
});
movement.forEach(function (move) {
    move.startTime = start_time;
});

秒针每转一圈需要 60000 毫秒,而分针比秒针慢 60 倍。

为了将时钟指针的操作与相同的时间概念联系起来(以确保分针在秒针完成旋转的那一刻准确地更新其位置),我使用了 startTime 属性。

另一方面,数字时钟有点违反直觉。每个数字都是一个带有溢出的容器:overflow: hidden;。在里面,有一排从零到一的数字坐在等宽的单元格中。通过将行水平平移单元格的宽度乘以数字值来显示每个数字。与模拟时钟上的指针一样,这是为每个数字设置正确持续时间的问题。虽然从毫秒到分钟的所有数字都很容易做到,但小时数需要一些技巧。

让我们看一下 start_time 变量的值:

?
1
2
3
4
5
6
const start_time = (function () {
    const time = new Date();
    const hour_diff = time.getHours() - time.getUTCHours();
    const my_current_time = (Number(time) % d) + (hour_diff * h);
    return document.timeline.currentTime - my_current_time;
}());

为了计算所有元素必须开始的确切时间,我取了 Date.now() 的值(自 1970 年 1 月 1 日以来的毫秒数),从中去掉一整天,并通过 与 UTC 时间的差异。 这给我留下了自今天开始以来经过的毫秒数。 这是我的时钟需要显示的唯一数据:小时、分钟和秒。

为了将该值转换为正常格式,我需要根据从加载此页面到调用 Date.now() 所经过的时间来调整它。 为此,我从 currentTime 中减去它。

总结

动画共享相同的时间参考,通过调整它们的 startTime 属性,你可以将它们与你需要的任何模式对齐。

Web Animations API 带有强大的 API,可让你显着减少工作量。 它还具有精确度,为实现一些需要精确性的应用程序提供了可能性。

希望我在本文中提供的示例能让你更好地了解它。

以上就是Web Animations API实现一个精确计时的时钟示例的详细内容,更多关于Web Animations API时钟计时的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7125040460455739406

延伸 · 阅读

精彩推荐
  • js教程微信小程序自定义modal弹窗组件的方法详解

    微信小程序自定义modal弹窗组件的方法详解

    这篇文章主要给大家介绍了关于微信小程序自定义modal弹窗组件的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学...

    遇见小美好12372021-12-15
  • js教程JS数组降维的几种方法详解

    JS数组降维的几种方法详解

    这篇文章主要介绍了JS数组降维的几种方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考...

    何其所幸5032022-02-25
  • js教程JavaScript仿京东轮播图效果

    JavaScript仿京东轮播图效果

    这篇文章主要为大家详细介绍了JavaScript仿京东轮播图效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    不是七七子5652022-01-24
  • js教程JS中箭头函数与this的写法和理解

    JS中箭头函数与this的写法和理解

    这篇文章主要给大家介绍了关于JS中箭头函数与this的写法和理解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需...

    limingru10462021-12-31
  • js教程JS实现鼠标移动拖尾

    JS实现鼠标移动拖尾

    这篇文章主要为大家详细介绍了JS实现鼠标移动拖尾效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    day010022021-12-21
  • js教程浅析JavaScript中的事件委托机制跟深浅拷贝

    浅析JavaScript中的事件委托机制跟深浅拷贝

    这篇文章主要介绍了JavaScript中的事件委托机制跟深浅拷贝,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要...

    打游戏也要有梦想5782022-01-05
  • js教程了不起的11个JavaScript代码重构最佳实践小结

    了不起的11个JavaScript代码重构最佳实践小结

    这篇文章主要介绍了了不起的11个JavaScript代码重构最佳实践小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需...

    pikapi3892021-12-29
  • js教程通过滑动翻页效果实现和移动端click事件问题

    通过滑动翻页效果实现和移动端click事件问题

    这篇文章主要介绍了滑动翻页效果实现和移动端click事件问题,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需...

    行舟客5482022-01-07