LoginSignup
4
2

More than 1 year has passed since last update.

Vue.jsでサクサク!SVG内に線を引いてみる

Last updated at Posted at 2019-08-11

はじめに

前回の記事続きです。
SVGとVue.jsを組み合わせて今回は、Pathを使って線を引いてみるという
処理を実装して見たいと思います。

前回の記事と組み合わせることで、
マインドマップのようなWebアプリも作れるかもしれないです。
(その場合はpath要素と図形要素を保持して、線を設定する形になると思います。)

aperture-video-95d3487f-079b-43c8-bca2-7f1eda3f892e (1).gif

早速コードを見ていきましょう!

Path要素を入れる

まず、線を表現するのに必要なPath要素を、
SVGタグ内に記載いたします。
v-model内にリストで表現し、線でつないだ分だけ可変的に生成できるようにします。

線を引いている時(確定前要素)と、線を引いた後(確定後要素)で、
Pathを分けております。
今回オプション要素で、線の太さや、色を全体に可変的に適用する設定もおまけで入れてます。

SVGDemo.vue
<template>
  <div class="container">
    <div class="setting-box">
      <!-- 色を変えるとこ -->
      <p></p>
      <input type="color" v-model="strokeFill" />
      <!-- 線の太さとか -->
      <p>線の太さ</p>
      <input type="range" v-model="strokeWidth" />
    </div>
    <!-- SVG定義 -->
    <svg :width="width" :height="height" :viewBox="viewport" @wheel="zoompan" @mousedown="lineNode($event, idx)"> 
      <!-- 線を引いた後 -->
      <path :d="path"
            fill="none"
            :stroke="strokeFill"
            :opacity="strokeOpacity"
            :stroke-width="strokeWidth"
            stroke-linecap="round"
            v-for="(path, idx) in pathList" :key="idx"
            v-if="pathList.length > 0"/>
      <!-- 線を引いている最中 -->
      <path :d="pathTemp"
            fill="none"
            :stroke="strokeFill"
            :opacity="strokeOpacity"
            :stroke-width="strokeWidth"
            stroke-linecap="round"
            v-if="isLineNode"/>
    </svg>
  </div>
</template>

イベントハンドラを実装

今回は図形からドラッグしていくと
線が生成され、図形同士で繋げた場合に
生成されるようにしていきます。

SVGDemo.vue

<script>
export default {
  name: 'SVGDemo',
  data () {
    return {
      width: 500,
      height: 500,
      ratio: 1,
      dx: 0,
      dy: 0,
      viewport: '0 0 500 500',
      isLineNode: false,
      beforeMouseX: null,
      beforeMouseY: null,
      selectIdx: 0,
      pathList: [],
      pathTemp: '',
      strokeFill: '#525',
      strokeOpacity: 1,
      strokeWidth: '5',
      moveLineX: 0,
      moveLineY: 0,
      startLineX: 0,
      startLineY: 0,
    } 
  },
  // マウス操作関連
  mounted () {
    console.log('MOUNT LISTENER ON')
    document.addEventListener('mouseup', this.mouseUp)
    document.addEventListener('mousemove', this.mouseMakeLine)
  },
  beforeDestroy () {
    console.log('MOUNT LISTENER OFF')
    document.removeEventListener('mouseup', this.mouseUp)
    document.removeEventListener('mousemove', this.mouseMakeLine)
  },
  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(' ')
    },
    // マウスのクリックが終わった段階で初期化
    mouseUp (e) {
      this.isLineNode = false
      this.beforeMouseX = null
      this.beforeMouseY = null
      this.pathList.push(this.pathTemp)
      this.pathTemp = 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()
    },
    lineNode (e, i) {
      this.startLineX = e.offsetX * this.ratio + this.dx
      this.startLineY = e.offsetY * this.ratio + this.dy
      this.moveLineX = e.offsetX * this.ratio + this.dx
      this.moveLineY = e.offsetY * this.ratio + this.dy
      this.isLineNode = true
      e.preventDefault()
    },
    mouseMakeLine (e) {
      if (!this.isLineNode) 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 = this.moveLineX
      var tempY = this.moveLineY
      tempX += dx
      tempY += dy
      if (tempX > 0) this.moveLineX = tempX
      if (tempY > 0) this.moveLineY = tempY
      this.pathTemp = 'M' + this.startLineX + ',' + this.startLineY + ' L' + this.moveLineX + ',' + this.moveLineY
    }
  }
}
</script>

完成

ここまでロジックを記載していくと
このような形になるかと思います。
aperture-video-65ade8d9-c974-4505-9dfc-b07807b73ed1.gif
ベジェ曲線など組み込んでカーブさせても
面白そうですね!

4
2
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
4
2