使用 CSS3 和 Vanilla JavaScript 的 HTML5 SVG 填充动画

已发表: 2022-03-10
快速总结↬在本文中,您可以从 Awwwards 网站了解如何构建动画笔记显示。 它讨论了 HTML5 SVG 圆形元素、它的笔画属性,以及如何使用 CSS 变量和 Vanilla JavaScript 对其进行动画处理。

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

自 2017 年 4 月起,CSS Level 3 Fill and Stroke Module 允许从外部样式表设置 SVG 颜色和填充模式,而不是在每个元素上设置属性。 在本教程中,我们将使用简单的普通十六进制颜色,但填充和描边属性也接受图案、渐变和图像作为值。

注意访问 Awwwards 网站时,动画笔记显示只能在浏览器宽度设置为 1024px 或更大的情况下查看。

注意显示项目演示
最终结果的演示(大预览)
  • 演示:笔记显示项目
  • 回购:注意显示回购
跳跃后更多! 继续往下看↓

文件结构

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

 mkdir note-display cd note-display touch index.html styles.css scripts.js

HTML

这是链接cssjs文件的初始模板:

 <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包含circlenote值和它的label

列表项元素和直接子元素
列表项元素及其直接子元素: .circle.percent.label 。 (大预览)

.circle_svg是一个 SVG 元素,包含两个 <circle> 元素。 第一个是要填充的路径,第二个是动画填充。

SVG 元素
SVG 元素。 SVG 包装器和圆形标签。 (大预览)

note分为整数和小数,因此可以对它们应用不同的字体大小。 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>

cxcy属性定义圆的 x 轴和 y 轴中心点。 r属性定义了它的半径。

您可能已经注意到类名称中的下划线/破折号模式。 那就是 BEM,代表blockelementmodifier 。 这是一种方法,可以让你的元素命名更有条理、更有条理和语义化。

推荐阅读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>

您一定会问自己TransparentReasonableUsableExemplary标签的含义是什么。 您对编程越熟悉,您就会意识到编写代码不仅是为了使应用程序正常运行,而且还要确保它是长期可维护和可扩展的。 这只有在您的代码易于更改时才能实现。

“首字母缩略词TRUE应该有助于确定您编写的代码是否能够适应未来的变化。”

所以,下一次,问问自己:

  • Transparent :代码更改的后果是否清晰?
  • Reasonable :成本效益值得吗?
  • Usable :我可以在意外情况下重复使用它吗?
  • Exemplary :它是否提供了高质量作为未来代码的示例?

注意Sandi Metz 的“Practical Object-Oriented Design in Ruby”解释了TRUE以及其他原则以及如何通过设计模式实现这些原则。 如果您还没有花时间学习设计模式,可以考虑将这本书添加到您的睡前阅读中。

CSS

让我们导入字体并对所有项目应用重置:

 @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 Sizing 让您的生活更轻松”。

 body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }

通过组合规则display: flexbodymargin-auto.display-container ,可以垂直和水平居中子元素。 .display-container元素也将是一个flex-container ; 这样,它的子节点将沿主轴放置在同一行中。

.note-display列表项也将是一个flex-container 。 由于要居中的孩子很多,让我们通过justify-contentalign-items属性来做。 所有flex-items都将沿cross轴和main居中。 如果您不确定它们是什么,请查看“CSS Flexbox Fundamentals Visual Guide”中的对齐部分。

 .note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }

让我们通过设置规则stroke-widthstroke-opacitystroke-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; }

为了绝对定位percent元素,有必要知道绝对是什么。 .circle元素应该是引用,所以让我们添加position: relative到它。

注意有关绝对定位的更深入、直观的解释,请阅读“如何一劳永逸地理解 CSS 绝对定位”。

另一种居中元素的方法是结合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-dasharraystroke-dashoffset

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

它最多可以采用四个值:

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

左图显示属性stroke-dasharray设置为 0 到 238px,即圆的周长。

第二个图像表示stroke-dashoffset属性,它偏移了 dash 数组的开头。 它也被设置为从 0 到圆周长度。

Stroke dasharray 和 dashoffset 属性
stroke-dasharray冲程-冲程偏移属性(大预览)

为了产生填充效果,我们将stroke-dasharray设置为圆周长度,这样它的所有长度都被一个大破折号填充而没有间隙。 我们还将用相同的值抵消它,所以它被“隐藏”了。 然后stroke-dashoffset将更新为相应的音符值,根据过渡持续时间填充笔画。

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

 .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变量被 100 毫秒 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); } 
描边属性转换
描边属性过渡(大预览)

现在,让我们计算dashoffset值——相对于音符。 note 值将通过 data-* 属性插入到每个li项目中。 *可以切换为适合您需要的任何名称,然后可以通过元素的数据集在 JavaScript 中检索它: 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的音符,我们将有(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); }); 
笔划属性过渡到音符值
笔划属性过渡到音符值(大预览)

在我们继续之前,让我们将 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); + }

注意值增加

仍然存在从0.00到要构建的音符值的音符过渡。 首先要做的是将整数和十进制值分开。 我们将使用字符串方法split() (它接受一个参数来确定字符串将在哪里被破坏并返回一个包含两个被破坏的字符串的数组)。 这些将被转换为数字并作为参数传递给increaseNumber()函数,以及display元素和一个指示它是整数还是小数的标志。

 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设置为900ms 。 现在,例如,要对从 0 到 7 的数字进行动画处理,持续时间必须除以音符900 / 7 = 128.57ms 。 结果表示每次增加迭代需要多长时间。 这意味着我们的setInterval将每128.57ms触发一次。

设置这些变量后,让我们定义setIntervalcounter变量将作为文本附加到元素并在每次迭代时增加:

 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); } 
无限计数器增加
无限计数器增加(大预览)

凉爽的! 它确实增加了价值,但它会永远这样做。 当notes达到我们想要的值时,我们需要清除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); } 
完成的笔记展示项目
完成的项目(大预览)

现在数字更新到音符值并使用clearInterval()函数清除。

这就是本教程的内容。 我希望你喜欢它!

如果您想构建一些更具交互性的东西,请查看我使用 Vanilla JavaScript 创建的记忆游戏教程。 它涵盖了基本的 HTML5、CSS3 和 JavaScript 概念,例如定位、透视、过渡、Flexbox、事件处理、超时和三元组。

快乐编码!