JavaScript
SVG
Vue.js
142
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

Organization

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

はじめに

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)といったモダンな技術を使って、
開発しておりますので、もしご興味がある方はぜひ、ご応募お待ちしております。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザー登録ログイン
masa0209
興味のある分野を手当たり次第何でも触ってみる、しがないエンジニアです。
akarie
ヒトの手とテクノロジーの力で、年齢(とし)を重ねながら、幸せに暮らせる社会の実現に貢献します
この記事は以下の記事からリンクされています

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
Azure Machine Learning を使って機械学習に関するナレッジをシェアしよう
~
Snykを使って開発者セキュリティに関する記事を投稿しよう!
~
142
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー