LoginSignup
124
142

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-04

はじめに

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

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

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

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

vueui.png

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

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

svg1 (1).gif

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

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

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

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

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>

最後に

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

現在弊社では、HRモンスターと呼ばれる
採用の新しいスタイルを提供するサービスをローンチいたしました。

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

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

124
142
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
124
142