
SVG — это мощный и гибкий графический формат, который идеально вписывается в веб-среду. В отличие от форматов растровых изображений, таких как JPG, PNG или GIF, SVG основан на векторах и состоит из «реальных» объектов, которые вы можете выбирать и манипулировать любым удобным для вас способом. Так что даже с некоторыми базовыми сценариями статическое изображение можно анимировать и сделать интерактивным. И это будет предметом этого урока.
SVG и Vue: идеальный матч
Чтобы продемонстрировать, как можно создавать сценарии SVG, я выбрал Vue.js. Причина моего выбора в том, что, на мой взгляд, SVG и Vue идеально подходят друг другу. Во-первых, Vue поддерживает SVG из коробки. И, во-вторых, SVG, как и HTML, основан на XML, поэтому мы можем применить систему реактивности Vue к SVG и сделать ее интерактивной так же просто и удобно, как и с шаблонами HTML.
Краткое руководство по интеграции Vue и SVG
Прежде чем мы перейдем к двум вариантам использования, которые мы рассмотрим ниже, позвольте мне дать вам представление о том, как работает интеграция SVG и Vue.
Для начала мы создадим базовый HTML-файл и включим фреймворк Vue. Затем мы помещаем SVG, которым хотим манипулировать, внутри.
|
1
2
3
4
5
|
<div id=»app»>
<svg width=»400″ height=»300″>
<rect @click=»toggleStroke» x=»10″ y=»10″ :width=»width» :height=»height» :fill=»color» stroke=»green» :stroke-width=»stroke»></rect>
</svg>
</div>
|
Здесь у нас есть прямоугольный объект, атрибуты которого связаны с объектом данных в экземпляре Vue. У нас также есть слушатель события щелчка, который вызывает метод toggleStroke() . Поэтому, когда мы нажимаем на прямоугольник, обводка будет переключаться.
А вот код Vue:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
new Vue({
el: ‘#app’,
data: {
color: ‘orange’,
width: 100,
height: 100,
stroke: 0
},
methods: {
toggleStroke(){
this.stroke == 0 ?
}
}
})
|
Как видите, Vue и SVG очень легко комбинировать. Теперь давайте рассмотрим несколько более реалистичных и полезных примеров.
Пример первый: создание виджета таймера обратного отсчета
В первом примере мы создадим виджет таймера обратного отсчета. Это позволит пользователям устанавливать минуты и секунды для данного периода времени, и при запуске таймер будет показывать оставшееся время в круговом режиме.
Чтобы нарисовать и анимировать прогресс, мы будем использовать объект круга SVG и его атрибут stroke-dasharray . Вы можете прочитать о технике кругового прогресса SVG здесь . Также, чтобы добавить некоторую структуру и приятный стиль, мы будем использовать компонент Card от Bulma . Поэтому убедитесь, что вы добавили фреймворк в ваш файл.
Мы начинаем с добавления компонента карты, а затем помещаем внутри заголовок с заголовком нашего виджета.
|
1
2
3
4
5
6
7
|
<div id=»app»>
<div class=»card»>
<header class=»card-header has-background-grey-darker»>
<p class=»card-header-title has-text-success»>COUNTDOWN TIMER</p>
</header>
</div>
</div>
|
Далее мы используем раздел изображения карты, чтобы поставить наш SVG.
|
1
2
3
4
5
6
7
8
9
|
<div class=»card-image»>
<svg xmlns=»http://www.w3.org/2000/svg» viewbox=»0 0 260 250″ width=»260″ height=»250″>
<rect x=»5″ y=»5″ width=»250″ height=»250″ fill=»orangered» />
<circle cx=»130″ cy=»125″ r=»80″ stroke=»lightsalmon» stroke-width=»10″ fill=»none» />
<circle cx=»130″ cy=»125″ r=»80″ stroke=»limegreen» :stroke-dasharray=»dasharray» stroke-offset=»600″
stroke-width=»10″ fill=»none» transform=»rotate(270,130,125)» />
<text x=»84″ y=»140″ fill=»white» font-size=»40″>{{ minute |
</svg>
</div>
|
Здесь у нас есть прямоугольник, который служит фоном. Мы используем два круга, чтобы создать круговой прогресс. Мы размещаем их так, чтобы они идеально перекрывали друг друга. Мы устанавливаем атрибут fill первого круга в none и используем только его обводку в качестве контура для прогресса.
Чтобы создать иллюзию рисования круга, мы связываем атрибут stroke-dasharray второго круга с вычисляемым свойством dasharray() , которое мы создадим чуть позже. Кроме того, мы хотим, чтобы начальная точка рисунка была в 12 часов, а не в 3 часа, что является значением по умолчанию. Для этого мы поворачиваем точку, используя атрибут transform . Последний объект — это текст, который мы размещаем в центре круга. Для правильного отображения времени с formatTime() нулем мы применяем фильтр formatTime() , который мы создадим позже.
Далее нам нужно добавить элементы управления для минут и секунд.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<div class=»card-content»>
<div class=»field is-horizontal»>
<div class=»field-label»>
<label class=»label is-size-7″>MINUTES:</label>
</div>
<div class=»field-body»>
<div class=»field»>
<div class=»control»>
<input class=»input is-success is-small» :disabled=»state===’started’ || state===’paused'» @change=»updateTime»
v-model=»minute» type=»number» name=»minutes» min=»0″ max=»59″ step=»1″>
</div>
</div>
</div>
</div>
<div class=»field is-horizontal»>
<div class=»field-label»>
<label class=»label is-size-7″>SECONDS:</label>
</div>
<div class=»field-body»>
<div class=»field»>
<div class=»control»>
<input class=»input is-success is-small» :disabled=»state===’started’ || state===’paused'» @change=»updateTime»
v-model=»second» type=»number» name=»seconds» min=»0″ max=»59″ step=»1″>
</div>
</div>
</div>
</div>
</div>
|
Важными элементами управления здесь являются входные данные, которые мы связываем с соответствующими свойствами Vue с помощью директивы v-model . Мы также отключаем их, когда state установлено как started или paused . Наконец, мы добавляем слушатель события изменения, который будет вызывать метод updateTime() .
И, наконец, мы добавляем кнопки для управления таймером.
|
1
2
3
4
5
6
7
|
<footer class=»card-footer»>
<div class=»buttons has-addons card-footer-item»>
<button class=»button is-success» :disabled=»state===’started’ || second==0 && minute==0″ @click=»start»><span>Start
<button class=»button is-success» :disabled=»state!==’started'» @click=»pause»>Pause</button>
<button class=»button is-success» :disabled=»state!==’started’ && state !== ‘paused'» @click=»stop»>Stop</button>
</div>
</footer>
|
Здесь мы снова добавляем прослушиватели событий щелчка и некоторые условия для кнопок, чтобы они были отключены, когда они не нужны.
Пока что нам понадобится немного CSS, чтобы исправить интервал и выравнивание некоторых частей таймера.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
#app {
width: 260px;
margin: 10px;
}
.card-header-title {
justify-content: center;
}
.card-content {
padding: 4px 20px 8px;
}
.card-footer-item {
padding: 4px;
}
|
А теперь пришло время добавить код Vue в уравнение.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
new Vue({
el: ‘#app’,
circumference: 2 * Math.PI * 80,
data: {
state: ‘stopped’,
minute: 0,
second: 0,
progress: 0,
timeInSeconds: 0
},
computed: {
dasharray(){
return this.progress + » » + this.$options.circumference
},
}
})
|
Сначала мы определяем необходимые свойства в объекте данных и добавляем окружность в виде пользовательской опции экземпляра Vue. Последнее потому, что нам нужно, чтобы circumference была статичной, а не реактивной. Мы создаем dasharray() вычисляемый для вычисления значений атрибута stroke-dasharray .
Теперь давайте добавим методы:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
methods: {
updateTime(){
this.timeInSeconds = Number(this.minute) * 60 + Number(this.second)
},
start() {
this.state = «started»;
if (this.progress == 0){
this.progress = this.$options.circumference;
}
this._tick();
this.interval = setInterval(this._tick, 1000);
},
pause() {
this.state = «paused»;
clearInterval(this.interval);
},
stop() {
this.state = «stopped»;
clearInterval(this.interval);
this.minute = 0;
this.second = 0;
this.progress = 0;
},
_tick: function() {
//if second is 0 and minute is 0, clear the interval
if (this.minute == 0 && this.second == 0){
this.stop()
}
//update progress
let delta = (this.$options.circumference / this.timeInSeconds)
if ((this.progress — delta) < (delta / 2)){
this.progress = 0
} else {
this.progress -= delta
}
//if second is not 0, just decrement second
if (this.second !== 0) {
this.second—;
return;
}
//if second is 0 and minute is not 0, decrement minute and set second to 59
if (this.minute !== 0) {
this.minute—;
this.second = 59;
}
}
}
|
Метод updateTime() обновляет значение свойства timeInSeconds каждом изменении значений.
Метод start() изменяет state на started и вызывает метод _tick() каждую секунду.
Метод _tick() обрабатывает правильное обновление progress , minute и second .
Метод pause() меняет state на paused и останавливает часы, очищая интервал.
Метод stop() изменяет state на stopped , останавливает часы и сбрасывает progress , minute и second .
И, наконец, мы добавляем фильтр formatTime() для правильного отображения времени.
|
1
2
3
4
5
6
7
8
|
filters: {
formatTime: function(value) {
if (value >= 10) {
return value;
}
return «0» + value;
}
}
|
Вот и все! Мы успешно использовали функции реактивности Vue для преобразования нашего статического рисунка SVG в интерактивный таймер обратного отсчета. Давайте перейдем к следующему примеру.
Пример второй: создание инфографики SVG
В этом примере мы создадим небольшую инфографику, демонстрирующую, что такое адаптивный веб-дизайн и как он работает. Благодаря Vue мы сможем оживить нашу иллюстрацию SVG и сделать ее более реалистичной и увлекательной.
Я создал статические части инфографики в Illustrator, а затем экспортировал их как SVG. Затем я добавил динамические части вручную. Динамические части представляют собой три каркаса, которые имитируют представление одного и того же веб-дизайна на разных устройствах. Давайте создадим их сейчас.
Сначала давайте создадим объекты данных, необходимые для разных каркасов.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
const laptop = {
r1: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r2: {x: ‘115’, y: ‘350’, width: ‘200’, height: ’30’},
r3: {x: ‘115’, y: ‘390’, width: ‘370’, height: ’70’},
r4: {x: ‘115’, y: ‘470’, width: ‘110’, height: ’40’},
r5: {x: ‘245’, y: ‘470’, width: ‘110’, height: ’40’},
r6: {x: ‘375’, y: ‘470’, width: ‘110’, height: ’40’},
}
const tablet = {
r1: {x: ‘200’, y: ‘335’, width: ‘200’, height: ‘220’},
r2: {x: ‘215’, y: ‘350’, width: ‘100’, height: ’30’},
r3: {x: ‘215’, y: ‘385’, width: ‘170’, height: ’70’},
r4: {x: ‘215’, y: ‘460’, width: ’80’, height: ’40’},
r5: {x: ‘305’, y: ‘460’, width: ’80’, height: ’40’},
r6: {x: ‘215’, y: ‘505’, width: ’80’, height: ’40’},
}
const phone = {
r1: {x: ‘220’, y: ‘335’, width: ‘160’, height: ‘220’},
r2: {x: ‘225’, y: ‘340’, width: ‘150’, height: ’30’},
r3: {x: ‘225’, y: ‘375’, width: ‘150’, height: ’70’},
r4: {x: ‘225’, y: ‘450’, width: ‘150’, height: ’30’},
r5: {x: ‘225’, y: ‘485’, width: ‘150’, height: ’30’},
r6: {x: ‘225’, y: ‘520’, width: ‘150’, height: ’30’},
}
new Vue({
el: ‘#app’,
data: {
d: {
r1: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r2: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r3: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r4: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r5: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
r6: {x: ‘100’, y: ‘335’, width: ‘400’, height: ‘220’},
}
},
})
|
Каждый каркас состоит из шести прямоугольников, поэтому для каждого дизайна мы создаем отдельный объект данных с необходимыми значениями. В экземпляре Vue мы создаем еще один, который будет базовым объектом.
Теперь давайте создадим прямоугольники SVG, необходимые для каркасов, и свяжем их атрибуты со значениями данных:
|
1
2
3
4
5
6
|
<rect :x=»d.r1.x» :y=»d.r1.y» :width=»d.r1.width» :height=»d.r1.height» fill=»lightgrey» stroke=»grey» stroke-width=»5″/>
<rect :x=»d.r2.x» :y=»d.r2.y» :width=»d.r2.width» :height=»d.r2.height» fill=»blue» />
<rect :x=»d.r3.x» :y=»d.r3.y» :width=»d.r3.width» :height=»d.r3.height» fill=»cyan» />
<rect :x=»d.r4.x» :y=»d.r4.y» :width=»d.r4.width» :height=»d.r4.height» fill=»orange» />
<rect :x=»d.r5.x» :y=»d.r5.y» :width=»d.r5.width» :height=»d.r5.height» fill=»green» />
<rect :x=»d.r6.x» :y=»d.r6.y» :width=»d.r6.width» :height=»d.r6.height» fill=»red» />
|
Далее мы создаем метод анимации с помощью Tween.js . Поэтому убедитесь, что вы добавили эту библиотеку в свой файл. Мы используем хук жизненного цикла Vue create created() для первоначальной анимации каркаса от базового объекта до каркаса дизайна ноутбука.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
created(){
this.anim(laptop)
},
methods: {
anim(val){
function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
}
requestAnimationFrame(animate);
new TWEEN.Tween(this.d.r1).to(val.r1, 1000).start();
new TWEEN.Tween(this.d.r2).to(val.r2, 1000).start();
new TWEEN.Tween(this.d.r3).to(val.r3, 1000).start();
new TWEEN.Tween(this.d.r4).to(val.r4, 1000).start();
new TWEEN.Tween(this.d.r5).to(val.r5, 1000).start();
new TWEEN.Tween(this.d.r6).to(val.r6, 1000).start();
}
}
|
А теперь мы добавляем наложения на значки устройств, размещая над ними прозрачные прямоугольники SVG. Таким образом, область щелчка будет содержать целые значки, а не только их контуры. И, наконец, мы добавляем прослушиватели событий click, которые будут вызывать метод anim() с выбранным дизайном.
|
1
2
3
|
<rect @click=»anim(laptop)» x=»95″ y=»640″ width=»155″ height=»110″ fill=»transparent»/>
<rect @click=»anim(tablet)» x=»295″ y=»645″ width=»85″ height=»105″ fill=»transparent»/>
<rect @click=»anim(phone)» x=»435″ y=»660″ width=»60″ height=»90″ fill=»transparent»/>
|
Так что теперь, когда мы открываем инфографику, дизайн ноутбука будет отображаться плавно анимированным, а когда мы нажимаем на значки различных устройств, дизайн каркаса будет соответственно обновляться с плавной анимацией. Круто, да?
Вывод
Как видите, SVG в сочетании с Vue может быть очень мощным и гибким. Vue упрощает доступ к объектам SVG и манипулирование ими, а также делает их полностью интерактивными. Таким образом, вы можете оживить статическую графику SVG и сделать ее динамичной и более приятной для пользователей. Такое взаимодействие может значительно улучшить пользовательский опыт и общий вид вашего веб-сайта или приложения.