在 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">‣ 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 实现一个精确计时的时钟相关推荐

  1. CSS Animations vs Web Animations API

    作者:Ollie Williams 原文:CSS Animations vs Web Animations API 在 JavaScript 有一个原生动画 API 叫 Web Animations ...

  2. 什么?你还不知道Web Animations API

    JavaScript 中有一个用于动画的原生 API,称为 Web Animations API.在这篇文章中,我们将其称为 WAAPI. 在本文中,我们将比较 WAAPI 和用 CSS 完成的动画. ...

  3. Web动画API教程:可爱的运动路径(Motion Path)

    这是介绍浏览器中web动画API的系列教程的第五篇.如果你有什么问题/想法,或者发现我理解错了规范的内容,或是希望我在接下来的文章中对某部分内容进行探讨的话,请在Twitter给我留言吧~@dancw ...

  4. Web Animations

    什么是Web Animations? Web Animations defines a model for supporting animation and synchronization on th ...

  5. react钩子_使用Web动画API和React钩子创建高性能动画

    react钩子 以React 挂钩方式使用Web Animations API (又名WAAPI). 让我们在现代世界中创建高性能,灵活和可操作的网络动画. 希望你们

  6. corodva中使用高德地图web js api

    高德地图web js api有一个其他的地图插件都没有的功能,就是地图俯角可以达到80°,而其他的地图api(百度45°,mapbox60°),包括高德地图的Android sdk都无法达到这样的大角 ...

  7. 利用 Web Share API 将网页分享到 App(上)

    2013年加入去哪儿网技术团队,目前在大住宿事业部,技术委员会委员,大前端负责人.个人对移动端技术.工程化有浓厚兴趣. 随着移动互联网的发展,用户流量逐渐从 PC 上迁移到了手机上,为了更好的满足移动 ...

  8. Python编程入门实战:打造你的第一个Web应用

    简介 欢迎来到本篇博客--"Python编程入门实战:打造你的第一个Web应用".无论你是一个初学者,还是有一些编程基础的开发者,我相信你都可以在这篇文章中收获知识和启发. Pyt ...

  9. python 商城api编写_Python实战-编写Web App-Day8-编写API

    原标题:Python实战-编写Web App-Day8-编写API API就是把Web App的功能全部封装了,所以,通过API操作数据,可以极大地把前端和后端的代码隔离,使得后端代码易于测试,前端代 ...

最新文章

  1. R语言赋值操作符:<−、<<-、=、->、->>、:、%in%、%*%
  2. 使用span标签为文字设置单独样式
  3. TCP和UDP基本原理
  4. Mycat 月分片方法 - pursuer.chen - 博客园
  5. laravel authorize(授权)
  6. 【数据结构与算法】之深入解析“两两交换链表中的节点”的求解思路与算法示例
  7. uiautomator的坑和AAPT命令方式启动一个应用程序
  8. jpa onetoone_拥抱开源从表设计到 JPA 实现
  9. 十年测开如何理解自动化测试里的数据驱动、关键字驱动思路
  10. Searching the Web论文阅读
  11. Pyotorch自定义损失函数
  12. 《算法图解》第四章课后作业
  13. [转] - 浅谈数据分析和数据建模
  14. android bool转字符串,Android 知识点——当json传入字符串,使用Boolean接收时,GSON会将其转换为false...
  15. 单元测试 测试用例 用例测试文件golang的单元测试
  16. Vue el-upload插件上传批量文件
  17. 如何玩转私域引流?全链路拆解经典玩法和实战案例
  18. 使用Python和机器学习进行文本情感分类
  19. Centrifugo(实时消息服务器)介绍+demo
  20. Box2D 的 PTM_RATIO

热门文章

  1. plot函数设置颜色、字体
  2. 他山之玉 可以攻石——碧玉太湖石挂件赏
  3. Flutter BuildContext 探究
  4. android adb 查看ip地址命令
  5. jquery 特效 地址
  6. react html编辑器,wangEditor富文本编辑器+react+antd的使用
  7. Sublime text3/4格式化json快捷键不生效。
  8. 企业邮箱和vip邮箱的区别
  9. python 打印异常 - traceback.format_exc()
  10. Win7延迟90秒启动exe文件