首頁 > 軟體

Web Animations API實現一個精確計時的時鐘範例

2022-07-29 14:03:02

正文

在 JavaScript 中,當事情準時發生時,很自然地會想到使用計時器函數。 但是,當某件事由於其他事情依賴於它而在準確的時刻發生時,你很快就會發現計時器會存在一個不準時的問題。而本文所要介紹的 Web Animations API 可以在某些情況下替代計時器函數,同時保持精確。

當你需要處理精確的視覺呈現時,你就會發現你花費了太多時間來解決 JavaScript 無法準確解決程式碼何時實際執行的問題。

例如,下面就舉了一個計時器準確性的問題。

JavaScript 計時器問題

在 JavaScript 中,每個任務都會經過一個佇列。 包括你的程式碼、使用者互動、網路事件等都會放入各自的任務佇列,進行事件迴圈處理。 這麼做能夠保證任務按順序發生。例如,當事件觸發或計時器到期時,你在回撥中定義的任務將進入到佇列。 一旦事件迴圈輪到了它,你的程式碼就會被執行。

可是,當在任務佇列中執行計數器函數時,問題就會暴露了。

低精度

在將任務放入佇列之前,我們可以準確定義超時應該等待多長時間。 但是,我們無法預測的是目前佇列中會出現什麼。這是因為 setTimeout 保證在將事物放入佇列之前的最小延遲。 但是沒有辦法知道佇列中已經有什麼。

曾經我不得不為一個網站實現隨機翻轉圖塊,其中一個錯誤是由休眠標籤引起的。 因為每個圖塊都有自己的計時器,所以當標籤啟用時,它們都會同時觸發。那個案例如下程式碼所示:

<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>
#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 引入了時間線的概念。 預設情況下,所有動畫都與檔案的時間軸相關聯。 這意味著動畫共用相同的“內部時鐘”——即從頁面載入開始的時鐘。

共用時鐘使我們能夠協調動畫。無論是某種節奏還是一種模式,你都不必擔心某些事情會延遲或超前發生。

開始時間

要使動畫在某個時刻開始,請使用 startTime 屬性。 startTime 的值以頁面載入後的毫秒數為單位。 開始時間設定為 1000.5 的動畫將在檔案時間軸的 currentTime 屬性等於 1000.5 時開始播放。

你是否注意到開始時間值中的小數點了嗎? 是的,你可以使用毫秒的分數來精確時間。 但是,精確度取決於瀏覽器設定。

另一個有趣的事情是開始時間也可以是負數。 你可以自由地將其設定為未來的某個時刻或過去的某個時刻。 將該值設定為 -1000,你的動畫狀態就像頁面載入時已經播放了一秒鐘一樣。 對於使用者來說,動畫似乎在他們甚至還沒有考慮存取你的頁面之前就已經開始播放了。

下面我們給出一個範例一起來看下如何使用 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);
    });
}());

效果如下:

因為時鐘是一種精密儀器,所以我讓秒針和分針在它們對應的值發生變化的那一刻改變它們的位置。 下面的程式碼說明了如何進行精確計時:

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 變數的值:

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時鐘計時的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com