摘要在这篇文章中你将了解Awwwards网是怎样实现动画的。 本文介绍了HTML5 SVG中的circle 元素,它的stroke属性,以及如何使用CSS变量以及用 Vanilla JavaScript 为它们设置动画。

SVG是一种基于XML的,用于定义缩放矢量图形的标记语言。 它允许你通过在2D平面中确定的一组点来绘制路径、曲线和形状。 此外你还可以通过在这些路径上添加动态属性(例如笔触,颜色,粗细,填充等)来生成动画。

从2017年4月起,CSS Level 3 填充和描边模块(https://www.w3.org/TR/fill-stroke-3/)开始支持从外部样式表设置SVG颜色和填充图案,而不是在每个元素上设置属性。 在本教程中,我们将会使用简单的纯十六进制颜色,不过填充和描边属性也支持图案,渐变和图像作为值。

注意:访问Awwwards(https://www.awwwards.com/)网站时,你需要把浏览器宽度设置为1024px或更高的才能更好的下查看动画笔记显示。


最终的演示结果

  • 演示链接 (https://marina-ferreira.github.io/smashing-magazine-note-display-demo/)

  • 源代码 (https://github.com/marina-ferreira/smashing-magazine-note-display-demo)

文件结构
让我们从在终端中创建文件开始:

1 mkdir note-display
2 cd note-display
3 touch index.html styles.css scripts.js
HTML
这是连接css和js文件的初始模板:

1<html lang="en"> 2<head> 3  <meta charset="UTF-8"> 4 5  <title>Note Display</title> 6 7  <link rel="stylesheet" href="./styles.css"> 8</head> 9<body>10  <script src="./scripts.js"></script>11</body>12</html>

每个note元素都包含一个列表项:li用于保存circle,note值及其label。


列出项元素及其直接子元素:.circle, .percent 和 .label
.circle_svg是一个SVG元素,它包含两个 元素。 第一个是要填充的路径,第二个用来为动画作准备。


SVG元素:SVG包装器和圆形标签。
注释分为整数和小数,所以可以把它们设定为不同大小的字体。 label 是一个简单的<span>。 把所有得这些元素放在一起看起来像这样:

 1<li class="note-display"> 2  <div class="circle"> 3    <svg width="84" height="84" class="circle__svg"> 4      <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> 5      <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> 6    </svg> 7 8    <div class="percent"> 9      <span class="percent__int">0.</span>10      <span class="percent__dec">00</span>11    </div>12  </div>1314  <span class="label">Transparent</span>15</li>

cx和cy属性定义圆的x轴和y轴中心点。 r属性定义其半径。

你可能已经注意到类名中的下划线/破折号模式。 这是BEM(block element modifier)(http://getbem.com/naming/),分别代表 block, element 和 modifier。 它是使元素命名更加结构化、有条理和语义化的一种方法。

推荐阅读:什么是BEM以及为什么需要它(https://www.smashingmagazine.com/2018/06/bem-for-beginners/)

为了完成模板结构,让我们将四个列表项包装在无序列表元素中:

无序列表包装器拥有四个li子元素
图:无序列表包装器拥有四个li子元素

1<ul class="display-container"> 2  <li class="note-display"> 3    <div class="circle"> 4      <svg width="84" height="84" class="circle__svg"> 5        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> 6        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> 7      </svg> 8 9      <div class="percent">10        <span class="percent__int">0.</span>11        <span class="percent__dec">00</span>12      </div>13    </div>1415    <span class="label">Transparent</span>16  </li>1718  <li class="note-display">19    <div class="circle">20      <svg width="84" height="84" class="circle__svg">21        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>22        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>23      </svg>2425      <div class="percent">26        <span class="percent__int">0.</span>27        <span class="percent__dec">00</span>28      </div>29    </div>3031    <span class="label">Reasonable</span>32  </li>3334  <li class="note-display">35    <div class="circle">36      <svg width="84" height="84" class="circle__svg">37        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>38        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>39      </svg>4041      <div class="percent">42        <span class="percent__int">0.</span>43        <span class="percent__dec">00</span>44      </div>45    </div>4647    <span class="label">Usable</span>48  </li>4950  <li class="note-display">51    <div class="circle">52      <svg width="84" height="84" class="circle__svg">53        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>54        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>55      </svg>5657      <div class="percent">58        <span class="percent__int">0.</span>59        <span class="percent__dec">00</span>60      </div>61    </div>6263    <span class="label">Exemplary</span>64  </li>65</ul>

你必须先问一下自己 Transparent、 Reasonable、 Usable 和 Exemplary 标签都代表什么意思。 随着你对编程的不断熟悉,就会发现写代码不仅仅是为了能够使程序正常运行,还需要要确保它能够被长期维护和扩展。 这些只有在你的代码容易被修改时才能够实现。

    “缩略词TRUE应该能够帮助你确定自己编写的代码是否能够适应未来的变化。”

那么,下次问问你自己:

透明:代码更改后果是否明确?
合理:成本效益值得吗?
可用:我是否能够在意外情况下重复使用它?
示例:它是否以高质量作为未来代码的示例?

  • Transparent(透明):代码在修改后果是否明确?

  • Reasonable(合理):成本效益值得吗?

  • Usable(可用):我是否能够在不同的场景下重复使用它?

  • Exemplary(示例):未来它是否可以作为高质量作为代码范本?

注:Sandi Metz在《面向对象设计实践指南:Ruby语言描述》(https://book.douban.com/subject/25795276/)一书解释了TRUE和其他原则,以及如何通过设计模式实现它们。 如果你还没有开始研究设计模式,请考虑将此书放到自己的案头。

CSS

让我们导入字体并使其对所有内容生效:

1@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200');23* {4  padding: 0;5  margin: 0;6  box-sizing: border-box;7}

box-sizing: border-box 属性中包括填充与边框值到元素的总宽度和高度,所以更容易计算图形的范围。

注意:有关box-sizing的说明,请阅读“使用CSS Box让你更轻松”(https://medium.com/swlh/make-your-life-easier-with-css-box-sizing-3b6b2578bccd)。

 1body { 2  height: 100vh; 3  color: #fff; 4  display: flex; 5  background: #3E423A; 6  font-family: 'Nixie One', cursive; 7} 8 9.display-container {10  margin: auto;11  display: flex;12}

通过组合规则显示:body 中的 flex 和 .display-container 中的 margin-auto,可以将子元素垂直水平居中。 .display-container元素也将作为一个 flex-container; 这样,它的子元素会沿主轴被放置在同一行。

.note-display 列表项也将是一个 flex-container。 由于有很多子项被居中,所以我们可以通过 justify-content 和 align-items 属性来完成。 所有 flex-items 都将垂直水平居中。 如果你不确定它们是什么,请查看“CSS Flexbox 可视化指南”(https://medium.com/swlh/css-flexbox-fundamentals-visual-guide-1c467f480dac)中的对齐部分。

1.note-display {2  display: flex;3  flex-direction: column;4  align-items: center;5  margin: 0 25px;6}

让我们通过设置`stroke-width,stroke-opacity 和 stroke-linecap 将笔划应用于圆,这些规则会使画面动起来。 接下来,我们为每个圆添加一种颜色:

1.circle__progress { 2  fill: none; 3  stroke-width: 3; 4  stroke-opacity: 0.3; 5  stroke-linecap: round; 6} 7 8.note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } 9.note-display:nth-child(2) .circle__progress { stroke: #FF00AA; }10.note-display:nth-child(3) .circle__progress { stroke: #AA00FF; }11.note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }

为了绝对定位百分比元素,必须完全知道这些概念是什么。 .circle元素应该是引用,所以让我们为其添加添加 position: relative 。

注意:对绝对定位更深入、直观的解释,请阅读“一劳永逸的理解 CSS Position”一文。

另一种使元素居中的方法是把 top: 50%, left: 50% 和 transform: translate(-50%, -50%); 组合在一起, 将元素的中心定位在其父级中心。

1.circle { 2  position: relative; 3} 4 5.percent { 6  width: 100%; 7  top: 50%; 8  left: 50%; 9  position: absolute;10  font-weight: bold;11  text-align: center;12  line-height: 28px;13  transform: translate(-50%, -50%);14}1516.percent__int { font-size: 28px; }17.percent__dec { font-size: 12px; }1819.label {20  font-family: 'Raleway', serif;21  font-size: 14px;22  text-transform: uppercase;23  margin-top: 15px;24}

到目前为止,模板应如该是下面这个样子:

完成的模板元素和样式

填充过渡

可以在两个圆形SVG属性的帮助下创建圆形动画:stroke-dasharray 和 stroke-dashoffset。

       “stroke-dasharray 定义笔划中的虚线间隙模式。”

它最多可能需要四个值:

当它被设置为唯一的整数( stroke-dasharray:10 )时,破折号和间隙具有相同的大小;
对于两个值( stroke-dasharray:10 5 ),第一个应用于破折号,第二个应用于间隙;
第三种和第四种形式(stroke-dasharray:10 5 2 和 stroke-dasharray:10 5 2 3 )将产生各种样式的虚线和间隙。

stroke-dasharray属性值
左边的图像显示属性stroke-dasharray设置为 0 到圆周长度 238px。

第二个图像表示 stroke-dashoffset 属性,它抵消了dash数组的开头。 它的取值范围也是从0到圆周长度。

stroke-dasharray 和 stroke-dashoffset 属性
为了产生填充效果,我们将 stroke-dasharray 设置为圆周长度,以便它所有长度都能充满其冲刺范围而不留间隙。 我们也会用相同的值抵消它,这样会使它能够被“隐藏”。 然后,stroke-dashoffset 将更新为对应的说明文字,根据过渡持续时间填充其行程。

属性更新将通过CSS Variables在脚本中完成。 下面让我们声明变量并设置属性:

1.circle__progress--fill {2  --initialStroke: 0;3  --transitionDuration: 0;4  stroke-opacity: 1;5  stroke-dasharray: var(--initialStroke);6  stroke-dashoffset: var(--initialStroke);7  transition: stroke-dashoffset var(--transitionDuration) ease;8}

为了设置初始值并更新变量,让我们从使用 document.querySelectorAll 选择所有.note-display元素开始。 同时把 transitionDuration设置为900毫秒。

然后,我们遍历显示数组,选择它的 .circleprogress.circleprogress--fill 并提取HTML中的 r 属性集来计算周长。 有了它,我们可以设置初始的 --dasharray 和 --dashoffset 值。

当 --dashoffset 变量被 setTimeout更新时,将发生动画:

1const displays = document.querySelectorAll('.note-display'); 2const transitionDuration = 900; 3 4displays.forEach(display => { 5  let progress = display.querySelector('.circle__progress--fill'); 6  let radius = progress.r.baseVal.value; 7  let circumference = 2 * Math.PI * radius; 8 9  progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);10  progress.style.setProperty('--initialStroke', circumference);1112  setTimeout(() => progress.style.strokeDashoffset = 50, 100);13});

要从顶部开始过度,必须旋转 .circle__svg 元素:

1.circle__svg {2  transform: rotate(-90deg);3}

Stroke 属性转换
现在,让我们计算相对于 note 的dashoffset值。 note 值将通过 data- 属性插入每个li项目。 可以替换为任何符合你需求的名称,然后可以通过元素的数据集在元数据集中检索:element.dataset.*。

注意:你可以在MDN Web Docs上得到有关 data-* 属性的更多信息。

我们的属性将被命名为 “data-note”:

1<ul class="display-container"> 2+ <li class="note-display" data-note="7.50"> 3    <div class="circle"> 4      <svg width="84" height="84" class="circle__svg"> 5        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> 6        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> 7      </svg> 8 9      <div class="percent">10        <span class="percent__int">0.</span>11        <span class="percent__dec">00</span>12      </div>13    </div>1415    <span class="label">Transparent</span>16  </li>1718+ <li class="note-display" data-note="9.27">19    <div class="circle">20      <svg width="84" height="84" class="circle__svg">21        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>22        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>23      </svg>2425      <div class="percent">26        <span class="percent__int">0.</span>27        <span class="percent__dec">00</span>28      </div>29    </div>3031    <span class="label">Reasonable</span>32  </li>3334+ <li class="note-display" data-note="6.93">35    <div class="circle">36      <svg width="84" height="84" class="circle__svg">37        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>38        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>39      </svg>4041      <div class="percent">42        <span class="percent__int">0.</span>43        <span class="percent__dec">00</span>44      </div>45    </div>4647    <span class="label">Usable</span>48  </li>4950+ <li class="note-display" data-note="8.72">51    <div class="circle">52      <svg width="84" height="84" class="circle__svg">53        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle>54        <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle>55      </svg>5657      <div class="percent">58        <span class="percent__int">0.</span>59        <span class="percent__dec">00</span>60      </div>61    </div>6263    <span class="label">Exemplary</span>64  </li>65</ul>

parseFloat方法将display.dataset.note返回的字符串转换为浮点数。 offset 表示达到最高值时缺失的百分比。 因此,对于 7.50 note,我们将得到 (10 - 7.50) / 10 = 0.25,这意味着 circumference 长度应该偏移其值的25%:

1let note = parseFloat(display.dataset.note);2let offset = circumference * (10 - note) / 10;

更新scripts.js:

 1const displays = document.querySelectorAll('.note-display'); 2const transitionDuration = 900; 3 4displays.forEach(display => { 5  let progress = display.querySelector('.circle__progress--fill'); 6  let radius = progress.r.baseVal.value; 7  let circumference = 2 * Math.PI * radius; 8+ let note = parseFloat(display.dataset.note); 9+ let offset = circumference * (10 - note) / 10;1011  progress.style.setProperty('--initialStroke', circumference);12  progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);1314+ setTimeout(() => progress.style.strokeDashoffset = offset, 100);15});

sroke属性转换为note值
在继续之前,让我们将stoke转换提取到它自己的方法中:

1const displays = document.querySelectorAll('.note-display'); 2const transitionDuration = 900; 3 4displays.forEach(display => { 5- let progress = display.querySelector('.circle__progress--fill'); 6- let radius = progress.r.baseVal.value; 7- let circumference = 2 * Math.PI * radius; 8  let note = parseFloat(display.dataset.note); 9- let offset = circumference * (10 - note) / 10;1011- progress.style.setProperty('--initialStroke', circumference);12- progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);1314- setTimeout(() => progress.style.strokeDashoffset = offset, 100);1516+ strokeTransition(display, note);17});1819+ function strokeTransition(display, note) {20+   let progress = display.querySelector('.circle__progress--fill');21+   let radius = progress.r.baseVal.value;22+   let circumference = 2 * Math.PI * radius;23+   let offset = circumference * (10 - note) / 10;2425+   progress.style.setProperty('--initialStroke', circumference);26+   progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);2728+   setTimeout(() => progress.style.strokeDashoffset = offset, 100);29+ }

注意增长值

还有一件事就是把 note 从0.00转换到要最终的 note 值。 首先要做的是分隔整数和小数值。 可以使用字符串方法split()。 之后它们将被转换为数字,并作为参数传递给 increaseNumber() 函数,通过整数和小数的标志正确显示在对应元素上。

 1const displays = document.querySelectorAll('.note-display'); 2const transitionDuration = 900; 3 4displays.forEach(display => { 5  let note = parseFloat(display.dataset.note); 6+ let [int, dec] = display.dataset.note.split('.'); 7+ [int, dec] = [Number(int), Number(dec)]; 8 9  strokeTransition(display, note);1011+ increaseNumber(display, int, 'int');12+ increaseNumber(display, dec, 'dec');13});

在 increaseNumber() 函数中,我们究竟选择 .percentint 还是 .percentdec 元素,取决于 className ,以及输出是否应包含小数点。 接下来把transitionDuration设置为900毫秒。 现在,动画表示从0到7的数字,持续时间必须除以note 900 / 7 = 128.57ms。 结果表示每次增加迭代将花费多长时间。 这意味着 setInterval将每隔 128.57ms 触发一次。

设置好这些变量后,接着定义setInterval。 counter 变量将作为文本附加到元素,并在每次迭代时增加:

 1function increaseNumber(display, number, className) { 2  let element = display.querySelector(`.percent__${className}`), 3      decPoint = className === 'int' ? '.' : '', 4      interval = transitionDuration / number, 5      counter = 0; 6 7  let increaseInterval = setInterval(() => { 8    element.textContent = counter + decPoint; 9    counter++;10  }, interval);11}


图:计数增长
太酷了! 确实增加了计数值,但它在无限循环播放。 当note达到我们想要的值时,还需要清除setInterval。 可以通过clearInterval函数完成:

 1function increaseNumber(display, number, className) { 2  let element = display.querySelector(`.percent__${className}`), 3      decPoint = className === 'int' ? '.' : '', 4      interval = transitionDuration / number, 5      counter = 0; 6 7  let increaseInterval = setInterval(() => { 8+   if (counter === number) { window.clearInterval(increaseInterval); } 910    element.textContent = counter + decPoint;11    counter++;12  }, interval);13}


最终完成
现在,数字更新到note值,并使用clearInterval()函数清除。

教程到此就结束了,希望你能喜欢它!

如果你想开发一些更具互动性的东西,请查看使用 Vanilla JavaScript 创建的Memory Game Tutorial 。 它涵盖了基本的HTML5,CSS3和JavaScript概念,如定位、透视、转换、Flexbox、事件处理、超时和三元组。

©著作权归作者所有:来自51CTO博客作者mb5ff980b461ced的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Web UI自动化测试之元素定位
  2. LeetCode #27 移除元素
  3. (美团)巧用数组下标,轻轻松松找出所有元素
  4. 超详细!详解一道高频算法题:数组中的第 K 个最大元素
  5. 动画:面试必刷之二维数组中查找一个元素
  6. 前 K 个高频元素告诉你桶排序有啥用
  7. Python办公自动化|光速对比并提取两份Word/Excel中的不同元素
  8. 动画: 快速排序 | 如何求第 K 大元素?

随机推荐

  1. MySQL基于Navicat的基本操作技巧
  2. MySql中有哪些存储引擎?
  3. 数据库行转列和列转行小例子
  4. mysql的zip版本安装填坑
  5. InnoDB辅助索引页面的物理结构是什么样子
  6. 一步一步学MySQL----9 条件数据记录查询
  7. mysql 操作索引FORCE INDEX
  8. navicat for mysql 传输数据失败
  9. 需要从mysql数据库中获得productdata。
  10. 在本地计算机无法启动MySQL服务。错误106