一步步教你用HTML5 SVG实现动画效果

37次阅读

共计 12252 个字符,预计需要花费 31 分钟才能阅读完成。

翻译:疯狂的技术宅原文:https://www.smashingmagazine….

本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章

摘要在这篇文章中你将了解 Awwwards 网是怎样实现动画的。本文介绍了 HTML5 SVG 中的 circle 元素,它的 stroke 属性,以及如何使用 CSS 变量以及用 Vanilla JavaScript 为它们设置动画。
SVG 是一种基于 XML 的,用于定义缩放矢量图形的标记语言。它允许你通过在 2D 平面中确定的一组点来绘制路径、曲线和形状。此外你还可以通过在这些路径上添加动态属性(例如笔触,颜色,粗细,填充等)来生成动画。
从 2017 年 4 月起,CSS Level 3 填充和描边模块开始支持从外部样式表设置 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, .percent 和 .label
.circle_svg 是一个 SVG 元素,它包含两个 <circle> 元素。第一个是要填充的路径,第二个用来为动画作准备。

图:SVG 元素:SVG 包装器和圆形标签
注释分为整数和小数,所以可以把它们设定为不同大小的字体。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 和其他原则,以及如何通过设计模式实现它们。如果你还没有开始研究设计模式,请考虑将此书放到自己的案头。
CSS
让我们导入字体并使其对所有内容生效:

* {
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、事件处理、超时和三元组。
祝你快乐的编码!????

本文首发微信公众号:jingchengyideng 欢迎关注,每天都给你推送新鲜的前端技术文章

正文完
 0