翻译:疯狂的技术宅
原文: https://www.smashingmagazine....
本文首发微信公众号:jingchengyideng
欢迎关注,天天都给你推送新鲜的前端技术文章javascript
摘要
在这篇文章中你将了解Awwwards网是怎样实现动画的。 本文介绍了HTML5 SVG中的circle 元素,它的stroke属性,以及如何使用CSS变量以及用 Vanilla JavaScript 为它们设置动画。
SVG是一种基于XML的,用于定义缩放矢量图形的标记语言。 它容许你经过在2D平面中肯定的一组点来绘制路径、曲线和形状。 此外你还能够经过在这些路径上添加动态属性(例如笔触,颜色,粗细,填充等)来生成动画。css
从2017年4月起,CSS Level 3 填充和描边模块开始支持从外部样式表设置SVG颜色和填充图案,而不是在每一个元素上设置属性。 在本教程中,咱们将会使用简单的纯十六进制颜色,不过填充和描边属性也支持图案,渐变和图像做为值。html
注意:访问 Awwwards网站时,你须要把浏览器宽度设置为1024px或更高的才能更好的查看动画显示。
让咱们从在终端中建立文件开始:前端
🌹 mkdir note-display 🌹 cd note-display 🌹 touch index.html styles.css scripts.js
HTML
这是链接css
和js
文件的初始模板:html5
<html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>
每一个note元素都包含一个列表项:li
用于保存circle
,note
值及其label
。java
图:列出项元素及其直接子元素:.circle
, .percent
和 .label
css3
.circle_svg
是一个SVG元素,它包含两个 <circle>元素。 第一个是要填充的路径,第二个用来为动画做准备。git
图:SVG元素:SVG包装器和圆形标签github
注释分为整数和小数,因此能够把它们设定为不一样大小的字体。 label
是一个简单的<span>
。 把全部得这些元素放在一块儿看起来像这样:编程
<li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>
cx
和cy
属性定义圆的x轴和y轴中心点。 r
属性定义其半径。
你可能已经注意到类名中的下划线/破折号模式。 这是BEM(block element modifier),分别表明 block
, element
和 modifier
。 它是使元素命名更加结构化、有条理和语义化的一种方法。
推荐阅读:什么是BEM以及为何须要它
为了完成模板结构,让咱们将四个列表项包装在无序列表元素中:
图:无序列表包装器拥有四个li
子元素
<ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
你必须先问一下本身 Transparent
、 Reasonable
、 Usable
和 Exemplary
标签都表明什么意思。 随着你对编程的不断熟悉,就会发现写代码不只仅是为了可以使程序正常运行,还须要要确保它可以被长期维护和扩展。 这些只有在你的代码容易被修改时才可以实现。
“缩略词
TRUE
应该可以帮助你肯定本身编写的代码是否可以适应将来的变化。”
那么,下次问问你本身:
透明:代码更改后果是否明确?
合理:成本效益值得吗?
可用:我是否可以在乎外状况下重复使用它?
示例:它是否以高质量做为将来代码的示例?
Transparent(透明)
:代码在修改后果是否明确?Reasonable(合理)
:成本效益值得吗?Usable(可用)
:我是否可以在不一样的场景下重复使用它?Exemplary(示例)
:将来它是否能够做为高质量做为代码范本?注:Sandi Metz在《面向对象设计实践指南:Ruby语言描述》一书解释了TRUE
和其余原则,以及如何经过设计模式实现它们。 若是你尚未开始研究设计模式,请考虑将此书放到本身的案头。
让咱们导入字体并使其对全部内容生效:
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
box-sizing: border-box
属性中包括填充与边框值到元素的总宽度和高度,因此更容易计算图形的范围。
注意:有关
*box-sizing
*的说明,请阅读
“使用CSS Box让你更轻松”_。
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
经过组合规则显示:body
中的 flex
和 .display-container
中的 margin-auto
,能够将子元素垂直水平居中。 .display-container
元素也将做为一个 flex-container
; 这样,它的子元素会沿主轴被放置在同一行。
.note-display
列表项也将是一个 flex-container
。 因为有不少子项被居中,因此咱们能够经过 justify-content
和 align-items
属性来完成。 全部 flex-items
都将垂直水平居中。 若是你不肯定它们是什么,请查看“CSS Flexbox 可视化指南”中的对齐部分。
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
让咱们经过设置`stroke-width
,stroke-opacity
和 stroke-linecap
将笔划应用于圆,这些规则会使画面动起来。 接下来,咱们为每一个圆添加一种颜色:
.circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }
为了绝对定位百分比元素,必须彻底知道这些概念是什么。 .circle
元素应该是引用,因此让咱们为其添加添加 position: relative
。
注意:对绝对定位更深刻、直观的解释,请阅读“一劳永逸的理解 CSS Position”一文。
另外一种使元素居中的方法是把 top: 50%
, left: 50%
和 transform: translate(-50%, -50%);
组合在一块儿, 将元素的中心定位在其父级中心。
.circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }
到目前为止,模板应如该是下面这个样子:
图:完成的模板元素和样式
能够在两个圆形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在脚本中完成。 下面让咱们声明变量并设置属性:
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
为了设置初始值并更新变量,让咱们从使用 document.querySelectorAll
选择全部.note-display
元素开始。 同时把 transitionDuration
设置为900毫秒。
而后,咱们遍历显示数组,选择它的 .circle__progress.circle__progress--fill
并提取HTML中的 r
属性集来计算周长。 有了它,咱们能够设置初始的 --dasharray
和 --dashoffset
值。
当 --dashoffset
变量被 setTimeout
更新时,将发生动画:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });
要从顶部开始过分,必须旋转 .circle__svg
元素:
.circle__svg { transform: rotate(-90deg); }
图:Stroke 属性转换
如今,让咱们计算相对于 note 的dashoffset
值。 note 值将经过 data-*
属性插入每一个li
项目。 *
能够替换为任何符合你需求的名称,而后能够经过元素的数据集在元数据集中检索:element.dataset.*
。
注意:你能够在MDN Web Docs上获得有关 data-*
属性的更多信息。
咱们的属性将被命名为 “data-note
”:
<ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
parseFloat
方法将display.dataset.note
返回的字符串转换为浮点数。 offset
表示达到最高值时缺失的百分比。 所以,对于 7.50
note,咱们将获得 (10 - 7.50) / 10 = 0.25
,这意味着 circumference
长度应该偏移其值的25%:
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
更新scripts.js:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); });
sroke属性转换为note值
在继续以前,让咱们将stoke转换提取到它本身的方法中:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }
还有一件事就是把 note 从0.00
转换到要最终的 note 值。 首先要作的是分隔整数和小数值。 可使用字符串方法split()
。 以后它们将被转换为数字,并做为参数传递给 increaseNumber()
函数,经过整数和小数的标志正确显示在对应元素上。
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });
在 increaseNumber()
函数中,咱们究竟选择 .percent__int
仍是 .percent__dec
元素,取决于 className
,以及输出是否应包含小数点。 接下来把transitionDuration
设置为900毫秒。 如今,动画表示从0到7的数字,持续时间必须除以note 900 / 7 = 128.57ms
。 结果表示每次增长迭代将花费多长时间。 这意味着 setInterval
将每隔 128.57ms
触发一次。
设置好这些变量后,接着定义setInterval
。 counter
变量将做为文本附加到元素,并在每次迭代时增长:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); }
图:计数增加
太酷了! 确实增长了计数值,但它在无限循环播放。 当note达到咱们想要的值时,还须要清除setInterval
。 能够经过clearInterval
函数完成:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); }
图:最终完成
如今,数字更新到note值,并使用clearInterval()
函数清除。
教程到此就结束了,但愿你能喜欢它!
若是你想开发一些更具互动性的东西,请查看使用 Vanilla JavaScript 建立的Memory Game Tutorial 。 它涵盖了基本的HTML5,CSS3和JavaScript概念,如定位、透视、转换、Flexbox、事件处理、超时和三元组。
祝你快乐的编码!🌹
欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章