Статьи

Превратите свои статичные рисунки SVG в интерактивные виджеты и инфографику с Vue.js

Конечный продукт
Что вы будете создавать

SVG — это мощный и гибкий графический формат, который идеально вписывается в веб-среду. В отличие от форматов растровых изображений, таких как JPG, PNG или GIF, SVG основан на векторах и состоит из «реальных» объектов, которые вы можете выбирать и манипулировать любым удобным для вас способом. Так что даже с некоторыми базовыми сценариями статическое изображение можно анимировать и сделать интерактивным. И это будет предметом этого урока.

Чтобы продемонстрировать, как можно создавать сценарии SVG, я выбрал Vue.js. Причина моего выбора в том, что, на мой взгляд, SVG и Vue идеально подходят друг другу. Во-первых, Vue поддерживает SVG из коробки. И, во-вторых, SVG, как и HTML, основан на XML, поэтому мы можем применить систему реактивности Vue к SVG и сделать ее интерактивной так же просто и удобно, как и с шаблонами HTML.

Прежде чем мы перейдем к двум вариантам использования, которые мы рассмотрим ниже, позвольте мне дать вам представление о том, как работает интеграция 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 в интерактивный таймер обратного отсчета. Давайте перейдем к следующему примеру.

В этом примере мы создадим небольшую инфографику, демонстрирующую, что такое адаптивный веб-дизайн и как он работает. Благодаря 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 и сделать ее динамичной и более приятной для пользователей. Такое взаимодействие может значительно улучшить пользовательский опыт и общий вид вашего веб-сайта или приложения.