实战使用 Web Animations API 实现一个精确计时的时钟

Posted 前端修罗场

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战使用 Web Animations API 实现一个精确计时的时钟相关的知识,希望对你有一定的参考价值。

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

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

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

JavaScript 计时器问题

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

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

低精度

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

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

<article id="demo">
  <section>
    <h2>Timeouts</h2>
    <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>
    <h2>Animations</h2>
    <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(varcss CSS Web Animations无前缀

Web API系列教材1.3 — 实战:用ASP.NET Web API和Angular.js创建单页面应用程序(下)

[目录]ASP.NET web api开发实战

#私藏项目实操分享# Spring专题「开发实战」Spring Security与JWT实现权限管控以及登录认证指南

Web API系列教程3.3 — 实战:处理数据(建立数据库)

Web API系列教程3.6 — 实战:处理数据(创建JavaScript客户端)