SVG是一种基于XML的,用于定义缩放矢量图形的标记语言。 它允许你通过在2D平面中确定的一组点来绘制路径、曲线和形状。 此外你还可以通过在这些路径上添加动态属性(例如笔触,颜色,粗细,填充等)来生成动画。
从2017年4月起,CSS Level 3 填充和描边模块(https://www.w3.org/TR/fill-stroke-3/)开始支持从外部样式表设置SVG颜色和填充图案,而不是在每个元素上设置属性。 在本教程中,我们将会使用简单的纯十六进制颜色,不过填充和描边属性也支持图案,渐变和图像作为值。
注意:访问Awwwards( https://www.awwwards.com/)网站时,你需要把浏览器宽度设置为1024px或更高的才能更好的下查看动画笔记显示。
让我们从在终端中创建文件开始:
1? mkdir note-display
2? cd note-display
3? touch index.html styles.css scripts.js
HTML
这是连接css
和js
文件的初始模板:
1
2
3
4
5 Note Display
6
7
8
9
10
11
12
每个note元素都包含一个列表项:li
用于保存circle
,note
值及其label
。
.circle_svg
是一个SVG元素,它包含两个 元素。 第一个是要填充的路径,第二个用来为动画作准备。
注释分为整数和小数,所以可以把它们设定为不同大小的字体。 label
是一个简单的。 把所有得这些元素放在一起看起来像这样:
1
2
3
7
8
9 0.
10 00
11
12
13
14 Transparent
15
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
子元素
1
2 -
3
4
8
9
10 0.
11 00
12
13
14
15 Transparent
16
17
18 -
19
20
24
25
26 0.
27 00
28
29
30
31 Reasonable
32
33
34 -
35
36
40
41
42 0.
43 00
44
45
46
47 Usable
48
49
50 -
51
52
56
57
58 0.
59 00
60
61
62
63 Exemplary
64
65
你必须先问一下自己 Transparent
、 Reasonable
、 Usable
和 Exemplary
标签都代表什么意思。 随着你对编程的不断熟悉,就会发现写代码不仅仅是为了能够使程序正常运行,还需要要确保它能够被长期维护和扩展。 这些只有在你的代码容易被修改时才能够实现。
“缩略词
TRUE
应该能够帮助你确定自己编写的代码是否能够适应未来的变化。”
那么,下次问问你自己:
透明:代码更改后果是否明确?
合理:成本效益值得吗?
可用:我是否能够在意外情况下重复使用它?
示例:它是否以高质量作为未来代码的示例?
Transparent(透明)
:代码在修改后果是否明确?Reasonable(合理)
:成本效益值得吗?Usable(可用)
:我是否能够在不同的场景下重复使用它?Exemplary(示例)
:未来它是否可以作为高质量作为代码范本?注:Sandi Metz在《面向对象设计实践指南:Ruby语言描述》(https://book.douban.com/subject/25795276/)一书解释了TRUE
和其他原则,以及如何通过设计模式实现它们。 如果你还没有开始研究设计模式,请考虑将此书放到自己的案头。
让我们导入字体并使其对所有内容生效:
1@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200');
2
3* {
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}
15
16.percent__int { font-size: 28px; }
17.percent__dec { font-size: 12px; }
18
19.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毫秒。
然后,我们遍历显示数组,选择它的 .circle__progress.circle__progress--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);
11
12 setTimeout(() => progress.style.strokeDashoffset = 50, 100);
13});
要从顶部开始过度,必须旋转 .circle__svg
元素:
1.circle__svg {
2 transform: rotate(-90deg);
3}
现在,让我们计算相对于 note 的dashoffset
值。 note 值将通过 data-*
属性插入每个li
项目。*
可以替换为任何符合你需求的名称,然后可以通过元素的数据集在元数据集中检索:element.dataset.*
。
注意:你可以在MDN Web Docs上得到有关 data-*
属性的更多信息。
我们的属性将被命名为 “data-note
”:
1
2+ -
3
4
8
9
10 0.
11 00
12
13
14
15 Transparent
16
17
18+ -
19
20
24
25
26 0.
27 00
28
29
30
31 Reasonable
32
33
34+ -
35
36
40
41
42 0.
43 00
44
45
46
47 Usable
48
49
50+ -
51
52
56
57
58 0.
59 00
60
61
62
63 Exemplary
64
65
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;
10
11 progress.style.setProperty('--initialStroke', circumference);
12 progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);
13
14+ setTimeout(() => progress.style.strokeDashoffset = offset, 100);
15});
在继续之前,让我们将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;
10
11- progress.style.setProperty('--initialStroke', circumference);
12- progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);
13
14- setTimeout(() => progress.style.strokeDashoffset = offset, 100);
15
16+ strokeTransition(display, note);
17});
18
19+ 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;
24
25+ progress.style.setProperty('--initialStroke', circumference);
26+ progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`);
27
28+ 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);
10
11+ increaseNumber(display, int, 'int');
12+ increaseNumber(display, dec, 'dec');
13});
在 increaseNumber()
函数中,我们究竟选择 .percent__int
还是 .percent__dec
元素,取决于 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); }
9
10 element.textContent = counter + decPoint;
11 counter++;
12 }, interval);
13}
现在,数字更新到note值,并使用clearInterval()
函数清除。
教程到此就结束了,希望你能喜欢它!
如果你想开发一些更具互动性的东西,请查看使用 Vanilla JavaScript 创建的Memory Game Tutorial 。 它涵盖了基本的HTML5,CSS3和JavaScript概念,如定位、透视、转换、Flexbox、事件处理、超时和三元组。