ライブラリ不要！Vue.jsとSVGで簡単に動かせるUIを作る

## はじめに

Vue.jsを普段開発で使っていて、SVGとの相性がすごく良いと感じてます。
Vue.jsの[ドキュメント](https://jp.vuejs.org/v2/cookbook/editable-svg-icons.html)でもアニメーションなどの紹介もされていますが、
今回はブラウザで編集可能なGUIの土台として、
使えそうなTipsを書いていきたいと思います。

## Vue.jsのプロジェクトを作成していく

今回使っていくのはVueCLI v3.xの環境で開発していきます。
標準でプロジェクト作成のUIなども付いており、非常に開発しやすいです。

```
$ vue ui
🚀  Starting GUI...
🌠  Ready on http://localhost:8000

```

<img width="1440" alt="vueui.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258186/7af484da-1141-d2c9-a1a3-c5314c95aa84.png">

## SVG内でズーム・パン風の機能を実装してみる

早速ですが、SVG内でズーム・パン風の機能を作って見たいと思います。
下記のような感じになります。
（MacBookProなどのノートPCですと二本指で開いてズームしたり、
　二本指でスライドして、移動したりして、操作が実感しやすいと思います。）

![svg1 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258186/90cdf0f0-ff33-16fe-1668-411428a90260.gif)

コードで実際に実装するとこのような形です。
移動の場合は差分値分移動されるようdeltaX, deltaYで計算します。
※CSSは割愛してます。

```vuejs:SVGDemo.vue
<template>
  <div class="container">
    <!-- SVG定義 -->
    <svg :width="width" :height="height" :viewBox="viewport" @wheel="zoomPan">
      <rect fill="blue" x="10" y="10" width="100" height="100"></rect>
    </svg>
  </div>
</template>

<script>
export default {
  name: 'SVGDemo',
  data () {
    return {
      width: 500,
      height: 500,
      ratio: 1,
      dx: 0,
      dy: 0,
      viewport: '0 0 500 500',
    } 
  },
  methods: {
    zoomPan (e) {
      var [x, y, w, h] = this.viewport.split(' ').map(v => parseFloat(v))
      if (e.ctrlKey) {
        // 拡大（Y軸が上がる場合） 縮小（Y軸が下がる場合）
        if (e.deltaY > 0) {
           w = w * 1.01
           h = h * 1.01
        } else {
          w = w * 0.99
          h = h * 0.99
        }
        this.makeViewBox(x, y, w, h)
        this.ratio = w / this.width
        e.preventDefault()
      } else {
        // 移動
        if ((this.dx + e.deltaX > -this.width && this.dy + e.deltaY > -this.width) &&
            (this.dx + e.deltaX < this.width * 2 && this.dy + e.deltaY < this.width * 2)) {
          this.makeViewBox(x + e.deltaX, y + e.deltaY, w, h)
          this.dx += e.deltaX
          this.dy += e.deltaY
        }
      }
    },
    // viewboxを作成
    makeViewBox (x, y, w, h) {
      this.viewport = [x, y, w, h].join(' ')
    }
  }
}
</script>

```

## ドラッグ＆ドロップでSVG内の図形を動かしてみる

図形を自由に動かしてみます。
下記のような感じの動きになります。

![svg (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258186/c262981c-594f-7d8a-4f38-3465934865b0.gif)


コードの実装としては、
ロジックが少し綺麗でないかもしれないですが、
図形をクリックした段階のものをindex指定し、
マウスが離されるまで、フラグを持っておきます。

フラグ中は、動かした時に差分を読み取りながら、
図形の座標を変更していきます。

```vue:SVGDemo.vue
<template>
  <div class="container">
    <!-- SVG定義 -->
    <svg :width="width" :height="height" :viewBox="viewport" @wheel="zoompan">
      <rect v-for="(r, idx) in rects" :key="idx"
        @mousedown="move($event, idx)"
        :fill="r.color"
        :x="r.x" :y="r.y" :width="r.w" :height="r.h">
      </rect>
    </svg>
  </div>
</template>

<script>
export default {
  name: 'SVGDemo',
  data () {
    return {
      width: 500,
      height: 500,
      ratio: 1,
      dx: 0,
      dy: 0,
      viewport: '0 0 500 500',
      isMove: false,
      beforeMouseX: null,
      beforeMouseY: null,
      selectIdx: 0,
      rects: [
        {
          x: 10,
          y: 10,
          w: 100,
          h: 100,
          color: 'green'
        },
        {
          x: 200,
          y: 150,
          w: 100,
          h: 100,
          color: 'red'
        },
        {
          x: 310,
          y: 410,
          w: 200,
          h: 100,
          color: 'blue'
        },
      ]
    } 
  },
  // マウス操作関連
  mounted () {
    console.log('MOUNT LISTENER ON')
    document.addEventListener('mouseup', this.mouseUp)
    document.addEventListener('mousemove', this.mouseMove)
  },
  beforeDestroy () {
    console.log('MOUNT LISTENER OFF')
    document.removeEventListener('mouseup', this.mouseUp)
    document.removeEventListener('mousemove', this.mouseMove)
  },
  methods: {
    zoompan (e) {
      var [x, y, w, h] = this.viewport.split(' ').map(v => parseFloat(v))
      if (e.ctrlKey) {
        // 拡大（Y軸が上がる場合） 縮小（Y軸が下がる場合）
        if (e.deltaY > 0) {
           w = w * 1.01
           h = h * 1.01
        } else {
          w = w * 0.99
          h = h * 0.99
        }
        this.makeViewBox(x, y, w, h)
        this.ratio = w / this.width
        e.preventDefault()
      } else {
        // 移動
        if ((this.dx + e.deltaX > -this.width && this.dy + e.deltaY > -this.width) &&
            (this.dx + e.deltaX < this.width * 2 && this.dy + e.deltaY < this.width * 2)) {
          this.makeViewBox(x + e.deltaX, y + e.deltaY, w, h)
          this.dx += e.deltaX
          this.dy += e.deltaY
        }
      }
    },
    // viewboxを作成
    makeViewBox (x, y, w, h) {
      this.viewport = [x, y, w, h].join(' ')
    },
    // 図形を動かすフラグを立てる
    move (e, i) {
      this.isMove = true
      this.selectIdx = i
      e.preventDefault()
    },
    // マウスのクリックが終わった段階で初期化
    mouseUp (e) {
      this.isMove = false
      this.beforeMouseX = null
      this.beforeMouseY = null
      e.preventDefault()
    },
    // move中は前回まで動かした差分を取りながら座標を変化させていく
    mouseMove (e) {
      if (!this.isMove) return
      var mouseX = e.offsetX * this.ratio + this.dx
      var mouseY = e.offsetY * this.ratio + this.dy
      var dx = 0
      var dy = 0
      if (this.beforeMouseX && this.beforeMouseY) {
          dx = mouseX - this.beforeMouseX
          dy = mouseY - this.beforeMouseY
      }
      this.beforeMouseX = mouseX
      this.beforeMouseY = mouseY
      var tempX = dx + Number(this.rects[this.selectIdx].x)
      var tempY = dy + Number(this.rects[this.selectIdx].y)
      if (tempX > 0) this.rects[this.selectIdx].x = tempX
      if (tempY > 0) this.rects[this.selectIdx].y = tempY
      e.preventDefault()
    }
  }
}
</script>

```

## 最後に

いかがでしたでしょうか？
そこまで長いコードを書かずに動かしたり、
ズーム・パン風のものを実装することができました。

現在[弊社](https://akarie.co.jp/)では、[HRモンスター](https://hr-monster.io/)と呼ばれる
採用の新しいスタイルを提供するサービスをローンチいたしました。

ローンチ後のさらなる機能追加、改善などのPDCAサイクルを回すべく、
エンジニアを[募集](https://www.wantedly.com/projects/341182)しております。
https://www.wantedly.com/projects/341182

Kubernetes、Vue.js（Javascript）、Django（Python）といったモダンな技術を使って、
開発しておりますので、もしご興味がある方はぜひ、ご応募お待ちしております。