使用 CSS3 和 Vanilla JavaScript 的 HTML5 SVG 填充动画
已发表: 2022-03-10SVG 代表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
这是链接css
和js
文件的初始模板:
<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
。
.circle_svg
是一个 SVG 元素,包含两个 <circle> 元素。 第一个是要填充的路径,第二个是动画填充。
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>
cx
和cy
属性定义圆的 x 轴和 y 轴中心点。 r
属性定义了它的半径。
您可能已经注意到类名称中的下划线/破折号模式。 那就是 BEM,代表block
、 element
和modifier
。 这是一种方法,可以让你的元素命名更有条理、更有条理和语义化。
推荐阅读: BEM 的解释以及为什么需要它
为了完成模板结构,让我们将四个列表项包装在一个无序列表元素中:
<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 的“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: flex
在body
和margin-auto
在.display-container
,可以垂直和水平居中子元素。 .display-container
元素也将是一个flex-container
; 这样,它的子节点将沿主轴放置在同一行中。
.note-display
列表项也将是一个flex-container
。 由于要居中的孩子很多,让我们通过justify-content
和align-items
属性来做。 所有flex-items
都将沿cross
轴和main
居中。 如果您不确定它们是什么,请查看“CSS Flexbox Fundamentals Visual Guide”中的对齐部分。
.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; }
为了绝对定位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-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
设置为 0 到 238px,即圆的周长。
第二个图像表示stroke-dashoffset
属性,它偏移了 dash 数组的开头。 它也被设置为从 0 到圆周长度。
为了产生填充效果,我们将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
触发一次。
设置这些变量后,让我们定义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); }
凉爽的! 它确实增加了价值,但它会永远这样做。 当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、事件处理、超时和三元组。
快乐编码!