Web?Animations?API實現(xiàn)一個精確計時的時鐘示例
正文
在 JavaScript 中,當(dāng)事情準(zhǔn)時發(fā)生時,很自然地會想到使用計時器函數(shù)。 但是,當(dāng)某件事由于其他事情依賴于它而在準(zhǔn)確的時刻發(fā)生時,你很快就會發(fā)現(xiàn)計時器會存在一個不準(zhǔn)時的問題。而本文所要介紹的 Web Animations API 可以在某些情況下替代計時器函數(shù),同時保持精確。
當(dāng)你需要處理精確的視覺呈現(xiàn)時,你就會發(fā)現(xiàn)你花費(fèi)了太多時間來解決 JavaScript 無法準(zhǔn)確解決代碼何時實際執(zhí)行的問題。
例如,下面就舉了一個計時器準(zhǔn)確性的問題。
JavaScript 計時器問題
在 JavaScript 中,每個任務(wù)都會經(jīng)過一個隊列。 包括你的代碼、用戶交互、網(wǎng)絡(luò)事件等都會放入各自的任務(wù)隊列,進(jìn)行事件循環(huán)處理。 這么做能夠保證任務(wù)按順序發(fā)生。例如,當(dāng)事件觸發(fā)或計時器到期時,你在回調(diào)中定義的任務(wù)將進(jìn)入到隊列。 一旦事件循環(huán)輪到了它,你的代碼就會被執(zhí)行。
可是,當(dāng)在任務(wù)隊列中執(zhí)行計數(shù)器函數(shù)時,問題就會暴露了。
低精度
在將任務(wù)放入隊列之前,我們可以準(zhǔn)確定義超時應(yīng)該等待多長時間。 但是,我們無法預(yù)測的是目前隊列中會出現(xiàn)什么。這是因為 setTimeout 保證在將事物放入隊列之前的最小延遲。 但是沒有辦法知道隊列中已經(jīng)有什么。
曾經(jīng)我不得不為一個網(wǎng)站實現(xiàn)隨機(jī)翻轉(zhuǎn)圖塊,其中一個錯誤是由休眠標(biāo)簽引起的。 因為每個圖塊都有自己的計時器,所以當(dāng)標(biāo)簽激活時,它們都會同時觸發(fā)。那個案例如下代碼所示:
<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">‣ Run</button>
</article>
#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);
}
(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 引入了時間線的概念。 默認(rèn)情況下,所有動畫都與文檔的時間軸相關(guān)聯(lián)。 這意味著動畫共享相同的“內(nèi)部時鐘”——即從頁面加載開始的時鐘。
共享時鐘使我們能夠協(xié)調(diào)動畫。無論是某種節(jié)奏還是一種模式,你都不必?fù)?dān)心某些事情會延遲或超前發(fā)生。
開始時間
要使動畫在某個時刻開始,請使用 startTime 屬性。 startTime 的值以頁面加載后的毫秒數(shù)為單位。 開始時間設(shè)置為 1000.5 的動畫將在文檔時間軸的 currentTime 屬性等于 1000.5 時開始播放。
你是否注意到開始時間值中的小數(shù)點了嗎? 是的,你可以使用毫秒的分?jǐn)?shù)來精確時間。 但是,精確度取決于瀏覽器設(shè)置。
另一個有趣的事情是開始時間也可以是負(fù)數(shù)。 你可以自由地將其設(shè)置為未來的某個時刻或過去的某個時刻。 將該值設(shè)置為 -1000,你的動畫狀態(tài)就像頁面加載時已經(jīng)播放了一秒鐘一樣。 對于用戶來說,動畫似乎在他們甚至還沒有考慮訪問你的頁面之前就已經(jīng)開始播放了。
下面我們給出一個示例一起來看下如何使用 Web Animations API。
示例:精確計時的時鐘
這個例子是一個精確計時的時鐘,代碼如下:
<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>
: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;
}
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);
});
}());
效果如下:

因為時鐘是一種精密儀器,所以我讓秒針和分針在它們對應(yīng)的值發(fā)生變化的那一刻改變它們的位置。 下面的代碼說明了如何進(jìn)行精確計時:
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;
});
秒針每轉(zhuǎn)一圈需要 60000 毫秒,而分針比秒針慢 60 倍。
為了將時鐘指針的操作與相同的時間概念聯(lián)系起來(以確保分針在秒針完成旋轉(zhuǎn)的那一刻準(zhǔn)確地更新其位置),我使用了 startTime 屬性。
另一方面,數(shù)字時鐘有點違反直覺。每個數(shù)字都是一個帶有溢出的容器:overflow: hidden;。在里面,有一排從零到一的數(shù)字坐在等寬的單元格中。通過將行水平平移單元格的寬度乘以數(shù)字值來顯示每個數(shù)字。與模擬時鐘上的指針一樣,這是為每個數(shù)字設(shè)置正確持續(xù)時間的問題。雖然從毫秒到分鐘的所有數(shù)字都很容易做到,但小時數(shù)需要一些技巧。
讓我們看一下 start_time 變量的值:
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 日以來的毫秒數(shù)),從中去掉一整天,并通過 與 UTC 時間的差異。 這給我留下了自今天開始以來經(jīng)過的毫秒數(shù)。 這是我的時鐘需要顯示的唯一數(shù)據(jù):小時、分鐘和秒。
為了將該值轉(zhuǎn)換為正常格式,我需要根據(jù)從加載此頁面到調(diào)用 Date.now() 所經(jīng)過的時間來調(diào)整它。 為此,我從 currentTime 中減去它。
總結(jié)
動畫共享相同的時間參考,通過調(diào)整它們的 startTime 屬性,你可以將它們與你需要的任何模式對齊。
Web Animations API 帶有強(qiáng)大的 API,可讓你顯著減少工作量。 它還具有精確度,為實現(xiàn)一些需要精確性的應(yīng)用程序提供了可能性。
希望我在本文中提供的示例能讓你更好地了解它。
以上就是Web Animations API實現(xiàn)一個精確計時的時鐘示例的詳細(xì)內(nèi)容,更多關(guān)于Web Animations API時鐘計時的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中使用toLocaleString數(shù)字格式化處理詳解
這篇文章主要為大家介紹了JavaScript中使用toLocaleString數(shù)字格式化處理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
smartbanner.js實現(xiàn)可定制智能應(yīng)用橫幅使用示例
這篇文章主要為大家介紹了smartbanner.js實現(xiàn)可定制智能應(yīng)用橫幅使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

