Help us understand the problem. What is going on with this article?

ベジェ曲線を描画してVue.jsの素晴らしさを体感する

More than 1 year has passed since last update.

半年に一度ぐらいの周期でベジェ曲線を描きたくなる体質になって、今回が三度目のベジェです。以前はCSSアニメーションでベジェ曲線を描いたりしていたのですが、次はSVGと相性が良さそうなVue.jsを選びました。座標の変数を更新するだけでベジェ曲線をぐにゃぐにゃできるのは尊い。SVGだけでなく、マグロとの相性も良かったです。

ezgif.com-crop.gif
プレビューはこちら

ベジェ曲線とはいったい

Bézier_3_big.gif

Wikipediaより

アンカーを設定するだけで曲線が引ける魔法。ではなく、最小限の「座標」を与えるだけで滑らかに描画できる、コンピュータに優しい曲線のこと。CSSのイージングやIllustratorのペンツールなど、身近なところで色々と使われています。論文を書いたベジェさんの名前から来ているらしいです。

こちらのサイトを参考に、一度手書きをしてみると、より身近に感じられるかもしれません。が、座標を設定するだけで良い感じの曲線が引けるという認識があれば事足ります。

SVGのいいところ

<!-- 赤い四角と緑の円だと直感的にわかる -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="300" height="300">
  <rect width="100" height="100" fill="red" />
  <circle cx="150" cy="150" r="100" fill="green" />
</svg>

ハイパーテキストをマークアップするのがHTMLなら、グラフィックをマークアップするのがSVGです。簡単なコードであれば形をイメージできるうえに、JavaScriptから手軽に操作できます。

WebGLを含むCanvasは、できる事の幅でいえばSVGよりもはるかに上ですし、ベジェ曲線も用意されていますが、線を引くだけなら大げさな気がします。あとレスポンシブ対応が地味にめんどくさい。サクサク動くゲームとか、ゴリゴリ動くリッチなサイトをCanvasで作れる人はすごくすごいと思います。

そんなこんなで、今回はSVGを使います。

Vue.jsとSVG

SVGの実体はXMLなので、HTMLのようにVue.jsから操作できます。ベジェ曲線に必要な座標を変数として定義しておいて、それをリアルタイムで変更できるようにすれば、いい感じのベジェ曲線がVue.jsとSVGで実現できそうです。高まります。

実装

ezgif.com-crop.gif
プレビューはこちら

SVG

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="520" height="520" class="preview-svg">
  <!-- line -->
  <line :x1="p[0].x" :y1="p[0].y" :x2="p[1].x" :y2="p[1].y" stroke="#e0e0e0" stroke-width="4"/>
  <line :x1="p[2].x" :y1="p[2].y" :x2="p[3].x" :y2="p[3].y" stroke="#e0e0e0" stroke-width="4"/>
  <!-- bezier -->
  <path :d="`M${p[0].x} ${p[0].y} C ${p[1].x} ${p[1].y}, ${p[2].x} ${p[2].y}, ${p[3].x} ${p[3].y}`" stroke="#444" stroke-width="4" fill="transparent"/>
  <!-- point -->
  <circle :cx="p[0].x" :cy="p[0].y" r="10" fill="#666666" />
  <circle :cx="p[1].x" :cy="p[1].y" r="10" fill="#42b883" class="can-grab" @mousedown="mousedownPoint(1)" />
  <circle :cx="p[2].x" :cy="p[2].y" r="10" fill="#35495e" class="can-grab" @mousedown="mousedownPoint(2)" />
  <circle :cx="p[3].x" :cy="p[3].y" r="10" fill="#666666" />
</svg>

x1cx などのプロパティが図形の座標になるので、そこにあとでVue.jsで設定する変数を置いておきます。ベジェ曲線は <path> を移動させて(M)、座標を設定してあげる(C)だけで引けます。

チュートリアル - SVG | MDN

Vue.js

ベジェ曲線の点をマウスで掴んで動かすと、線がぐにゃぐにゃ変化して、マグロが泳ぐときのイージングも変わります。

マウスイベントの設定

イベント 動作 実装
mousedown マウスを押す 点を掴む
mousemove マウスを動かす 座標を更新する
mouseup マウスを離す 点を離す
html
<div
  :class="['container', {'is-grabbing': grabbingIndex}]"
  @mousemove="mousemoveContainer"
  @mouseup="mouseupContainer"
>
  <svg>
    <circle :cx="p[1].x" :cy="p[1].y" r="10" fill="#42b883" class="can-grab" @mousedown="mousedownPoint(1)" />
    <circle :cx="p[2].x" :cy="p[2].y" r="10" fill="#35495e" class="can-grab" @mousedown="mousedownPoint(2)" />
  </svg>
</div>
scss
.can-grab {
  cursor: grab; // 掴める
}
.container {
  &.is-grabbing {
    cursor: grabbing; // 掴んでる
    .can-grab {
      cursor: grabbing; // 掴んでる
    }
  }
}

js
mousedownPoint(index) {
  this.grabbingIndex = index
},
mousemoveContainer(e) {
  if (this.grabbingIndex) {
    const x = e.pageX - this.offset.x
    const y = e.pageY - this.offset.y

    this.setPosition(this.grabbingIndex, x, y)
  }
},
mouseupContainer() {
  this.grabbingIndex = null
}

ベジェ曲線の点をドラッグできるようにマウスイベントを設定していきます。素直に考えると、点に対してすべてのマウスイベントを設定したくなりますが、それだとマウスの動きが速くて点から離れたときに mousemove イベントが切れてしまいます。

なので、点には mousedown だけを設定して「どの点を掴んでいるか」を保持しておき、 mousemove はエリア全体に設定することで、素早くドラッグしても切れないベジェ曲線になりました。エリアの mouseup で「どの点を掴んでいるか」をリセットします。

タッチイベントの対応が面倒だったので、スマホは非対応です。

座標の更新

setPosition(index, x, y) {
  this.p.splice(index, 1, {
    x: this.kimurakaela(x),
    y: this.kimurakaela(y)
  });
}

点を掴んだ状態でマウスが動いたときに呼ばれるメソッドで、座標を更新する処理を行っています。Vue.jsでは、配列のインデックスを直接指定して値を更新することができないので、spliceメソッドを使用しました。

リストレンダリング | Vue.js

マグロにイージングを反映

html
<img :style="{animationTimingFunction: cubicBezier}" />
scss
.tuna {
  animation: 1.5s swim infinite;
}

@keyframes swim {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100vw) translateX(-100%);
  }
}
js
computed: {
  cubicBezier() {
    const b = [
      this.normalize(this.p[1].x),
      this.normalize(this.max - this.p[1].y),
      this.normalize(this.p[2].x),
      this.normalize(this.max - this.p[2].y)
    ].join(', ')

    return `cubic-bezier(${b})` // cubic-bezier(0.9, 0.1, 0.1, 0.9)
  }
}

マグロはCSSアニメーションで延々と泳いでいるのですが、それだけだと単調なので、ベジェ曲線の値をアニメーションのイージングに反映することで変化をつけています。

イージングの基本

変数として保持しているのは、SVG上の座標だけなので、CSSに指定するベジェ曲線の値を computed で算出しています。座標の値を0〜1に変換してY軸を反転させています。

コード

説明をわかりやすくするために省略している部分があるため、詳細はCodePenでご確認ください。

See the Pen bezier meets tuna by noplan1989 (@noplan1989) on CodePen.

まとめ

Vue.jsは使いやすくて、ベジェ曲線は綺麗だ。

nishinoshake
AWSと映画のサイトを作ってます。
https://noplan.cc
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした