Vue.js + SVG練習7: 文字がにょろっと出てくるやつ
動いてるところは下記ツイートから見れる(gifがでかくなりすぎてはてなにアップロードできなかった)
文字をアニメーションさせて遊ぶやつができた https://t.co/mTYNrkBLxi pic.twitter.com/UT4inhu0My
— はっしゅろっく (@hashedrock) 2017年11月18日
デモサイトは下記。文字を入力したり、文字サイズや太さを変えて遊べる。
https://hashrock.github.io/canvas-lff/
ソースは下記。
リポジトリについて
- もともとは昔に作ったリポジトリで、LFFというラインフォントフォーマットの存在を知って作ったやつ
- canvas上に描くようにして、光らせたりアニメーションさせたりはしていた
- 今回はVue + SVGに移植した
- ついでにES5 -> ES2015 -> TypeScriptに段階移行した
フォントについて
- LFFはCAD用のフォーマットらしく、プロッターとかで描画したり、物理的に掘ったりしてたんじゃないかなぁと思う。
- ここにあるKST32Bというフォントがレトロでかっこよかったので使わせて頂いた。GPL2。
- 内容としては、0-10までの範囲の点列をテキストで記述しているだけ
- なので、JSでゴリゴリパースした
SVG周りについて
- もともとcanvas向けに処理を書いていたので、楽をするためにd3-pathを利用した
- d3-pathを使うと、canvas互換のAPIで引いた線をpath要素にシリアライズすることが出来る
- (なんかy軸反転してたり、座標が細かい小数点になっている気がする。自分のバグかもしれんが)
アニメーションについて
- animejsを利用
- animejsとmo.jsは2大Exampleかっこいいけど全く使いこなせないライブラリと勝手にみなしている
- よくあるパスアニメーションを利用。点線ハックするやつ。SVGの仕様決めた人こういう使われ方想定してなかっただろうな
- 文字を重ねて3色で描き、それぞれずらしながら線を描画していけば色がついてかっこよくなる(mo.jsのexampleにあったテクニック)
Vueについて
- 今回は1行と1文字、それぞれをSFCにしてある
- 最初はフォントデータの検索が遅く、データ構造変えたりキャッシュを期待してcomputedにもたせてみたり、設計をがちゃがちゃ変えた
- 文字の太さ変更については、CSSプロパティとinput: rangeを紐付ける必要があったので、下記の知見を使った(CSS VariablesはJSから変更したときに再評価される)
CSS variable、JSから変更するとcalcの再評価走る!!!リアクティブ!!! https://t.co/ILcjch7OHH pic.twitter.com/OMLcCOVKul
— はっしゅろっく (@hashedrock) 2017年6月28日
思い
- これが何なのかはわからないけど、今までの中では結構遊んでて楽しいやつができた。
- すっごいアクセシビリティの荒廃したサイト作れそう
Vue.js + SVG練習6: ズーム&パンできる折れ線グラフを書く
ズーム&パンできる折れ線グラフを書いた。グリッドライン付き。
こういったグラフや、ズーム・パンはd3の得意技なのだけれど、あえてSVGを組み上げる部分をd3でやらずVueのテンプレート側でやっている。 スケーリングや、グリッドラインの位置決めなんかは、d3のコア部分の「d3-scale」というライブラリを利用した。
実際描画部分は全く苦労していなくて、グリッドラインも含めてたったのこれだけ。
<template> <svg :width="w" :height="h" @wheel="wheel" @pointerdown="startDrag" @pointermove="onDrag" @pointerup="stopDrag"> <polyline fill="none" stroke="#793" :points="points"></polyline> <line class="grid" v-for="(tick, index) in ticks" :key="index" :x1="scaleTick(tick)" :y1="0" :x2="scaleTick(tick)" :y2="h"></line> </svg> </template>
マウスホイールとか、ドラッグの挙動なんかはmixinとかで切り出せないかな、とかは思っている。 コードは以下。
ポイント
- d3-scaleはめちゃくちゃ便利で、
scale.scaleLinear().domain([min, max]).range([distmin, distmax])
とかやると、domain範囲の値をrange範囲にマッピングしてくれる。 - さらに、上記から取れるfunctionにtickというメソッドが生えていて、これは何かというとdistminからdistmaxまでの範囲で、ここに縦線引いたらいい感じになるよ、という座標の配列を返してくれる。
- ので、これをそのままグリッドラインに変換してしまえば終わり。
- 折れ線グラフを塗りつぶすには、pathの端っこを一旦y座標0に落としてから閉じてあげる必要がある。
- マウスホイールの挙動はd3-zoomからパクった。d3は参考になる箇所が無限にある
- マウスドラッグの挙動は、この前作ったスライダーからの流用。
- あといい機会なので全部TypeScriptに移行した。d3使うときは型があると涙がでるほどありがたい。
- 算数むずかし~~~ってなったときは、computedに切り出して変換後の変数をVue devtoolで見れるようにしてしまうと、どういう変化をするのかが分かりやすい。とにかくVueでロジックを簡単にするにはcomputedを使いこなすこと。
- 計算量については全く何も考えてないので、データ量が多くなったら万事休すかもしれない。そのあたりはおいおい勉強していきたい。
気持ち
そもそも、このSVGの練習を始めたのも色々理由があって、
- VueconfのSarah Drasnerさんの発表が良かったため
- anydownで使うSVGのUIの完成度を上げたかったため
- Vue.js meetupの時だったか、あまり覚えてないけど、「Vue + D3の連携をするより、VueとSVGで直接描画したほうがいい」と誰かが言っていたのを小耳に挟んだ為
この最後の理由も結構大きくて、検証のために今回の練習に至っている。 d3は便利なんだけどSVGの生成部分が辛く、メソッドチェーンをゴリゴリ書いてるうちに自分が一体何を書いているのかわからなくなりがちなのと、 既成のグラフコンポーネントを使ったら使ったで、カスタマイズが全くできなくなりがちというのが結構困るポイントだったりする。 Vueのテンプレートは大変書きやすいので、カスタマイズしやすいグラフが作れるのではないかな。
d3もやっていることはDOMのバインディングと生成で、Vueの出来ることとおんなじなので、Vue + D3の糊付けを頑張るよりは、Vueでやれる範囲を広げたほうがいいんじゃないかという感じに思っている。 無論、d3のutilは便利だし、再発明を防ぐためにd3の考え方の流用はやったほうが良いと思う。
あとは、すべてをVueのバインディングでやることで、複数のグラフのズーム位置やフィルタ状態などの同期が簡単に出来るんじゃないかともくろんでいる。 実際、Webアプリ上でグラフを単体で見たいということは少なく、複数グラフの比較やいろんな切り口でも分析に使うわけで、双方向バインディングが役に立つのでは。
Vue + TypeScript + poiで小さいプロジェクトを作る時の手順
TypeScriptを使いたい、型に守られたいという気持ちが定期的に生まれるのだけど、 ただwebpack.config.jsとtsconfig.jsonが上手く書けなくてしんどいみたいなことがままある。
お仕事的に、TypeScript + Vueでやっていくぞというのは下記のダーシノさんのスライドが良い。
www.slideshare.net
上記の内容と、webpack-simpleという自分が愛用しているテンプレートと、vue-class-componentのexampleを見ながら書いた雛形が下記に置いてある。
それはそれとして、今日はpoiを調べた。
poi + TypeScript
poiは設定より規約感のあるツールで、書捨てのプロジェクトにはwebpack直で使うよりpoiが楽だったりする。 .vueファイルへの対応も最初から組み込まれている。 デフォルトではbabelが使われる設定なのだけど、TypeScriptやる場合は、もう下記のことを順にやってけば良い。
で、それなりにボイラープレートも必要だったので、自分なりにやった手順を書いておく。
まずはセットアップ。
mkdir my-project cd $_ yarn init yarn add poi typescript poi-preset-typescript --dev
そして、下記のファイルを作る。
index.ts
import Vue from 'vue' import App from "./App.vue" new Vue({ el: '#app', render: h => h(App) })
tsconfig.json
{ "compilerOptions": { "target": "es5", "strict": true, "module": "es2015", "moduleResolution": "node", "experimentalDecorators": true } }
App.vue
<template> <div id="main"> <h1>Hello World!</h1> <input v-model="text"> <p>input: {{text}}</p> </div> </template> <script lang="ts"> import Vue from "vue"; import Component from "vue-class-component"; @Component({}) export default class App extends Vue { text: string = "Hello There"; mounted() {} } </script>
poi.config.js
module.exports = { presets: [ require('poi-preset-typescript')(/* options */) ], "homepage": "./" }
sfc.d.ts
declare module "*.vue" { import Vue from 'vue' export default Vue }
その後、package.json内に下記を追記。
"scripts": { "dev": "poi index.ts", "build": "poi build index.ts" }
もし、ビルド後のプロジェクトをGitHub Pagesにデプロイしたい場合は…
yarn add gh-pages
しつつ、package.jsonに以下も追加
"scripts": { "deploy": "gh-pages -d dist" }
という感じで、意外とボイラープレート作らなきゃいけなくて、 まだ楽とは言い切れない感じではある。
ただこれさえ済ませてしまえば、Vueファイルの中でもTypeScriptが使えてバイブス上がる感じになる。
という風に、補完もバッチリ効く。ただ、LSP実装もなかなか大変なようで、リネームやリファクタリングには未対応の様子だった。veturのロードマップ
VSCodeのリファクタ機能をフルに使いたい場合は、ロジックはなるべくモデルに切り出して、普通のTSにしてからリファクタするのが良さそう。
これで作ったプロジェクトの雛形が以下。流石に素のwebpackより大分シンプル。
気持ち
自分がコード書くときはとにかく設計も何もなく適当にぐちゃぐちゃ書き進めることが大半で、しかも意識が朦朧としていることが多かったりする。JSは集中してないと書けないので大変難しい。
自分がアホだからこそ役立つのがTypeScriptなんだけど、アホにとってtsconfig.jsonやwebpack.config.jsはパズルじみていて、書くのが難しいという問題がある。もうちょっと楽になったらいいなぁとは思う。
追記 : vue-class-componentを使わないバージョンのブランチを作った
vue-class-componentを使わなくとも、TypeScriptの恩恵を受けられるそうで、 お手軽用途にそっちのバージョンも作った。
data内のプロパティにもちゃんと型がついてる。すごい。
最初はbabelで書いて、TSに段階移行というやり方が取れると思う。
babelからの移行手順で必要なのは
- poi.config.js
- tsconfig.json
- sfc.d.ts
- コンポーネント定義時にVue.extendを使うように変更(これをしないと型推論がされない)
これによる利点は、下記を参考。
Vue.js + SVG練習5: クリックすると頑張るスピナー
ただし、やりすぎると爆発する。
動作確認は以下より。
ソースはこちら。
ポイント
- 小ネタなので特にないかな…
- 早めに寝たいので、短い時間で実装できる題材を結構考えた
- 小ジャンプは、2次関数でやるパターンと、y変位と速度でやるパターンがあったような気がするけど、頭回ってなくて後者でやった
- とにかく加速度は常に下にかけて、y変位は0を超えないようにすればジャンプになる。現実と同じ
日記
Vue.js + SVG練習4 : クリックで音の鳴るピアノ鍵盤
SVGをグラフィックエディタでいじる方針のリベンジをしたかったので、今回は下記のようなピアノを作った。
下記から動いているところを試せる(音が出ます)
前回Inkscapeで撃沈したあと、色々試した所、Method DrawというオンラインSVGエディタがだいぶ良さそうだった。
上記のグラフィックをサクッと作り、出力した結果が下記。
<svg width="20" height="100" xmlns="http://www.w3.org/2000/svg"> <!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ --> <defs> <linearGradient id="svg_4" x1="0.410156" y1="0.417969" x2="0.320313" y2="0.144531"> <stop stop-color="#fff" offset="0"/> <stop stop-color="#b7b7b7" stop-opacity="0.996094" offset="1"/> </linearGradient> <linearGradient id="svg_10" x1="0.558594" y1="0.785156" x2="0.144531" y2="0"> <stop stop-color="#666666" offset="0"/> <stop stop-color="#ffffff" stop-opacity="0.996094" offset="1"/> </linearGradient> </defs> <g> <title>background</title> <rect fill="#fff" id="canvas_background" height="18.666661" width="5.333332" y="-1" x="-1"/> <g display="none" id="canvasGrid"> <rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%" id="svg_2"/> </g> </g> <g> <title>Layer 1</title> <rect stroke="#000" id="svg_1" height="100" width="20" x="-0.333333" fill="url(#svg_4)"/> <rect fill="#000000" stroke="#000" stroke-opacity="null" width="12" height="60" id="svg_5"/> <rect fill="#666666" stroke-width="0" stroke-opacity="null" x="11" y="0.333334" width="1" height="59" id="svg_6" stroke="#000"/> <rect fill="url(#svg_10)" stroke-width="0" stroke-opacity="null" x="0.833333" y="56" width="10.5" height="3.666667" id="svg_8" stroke="#000"/> </g> </svg>
Inkscapeから比べると遥かにクリーンで言うことなし。あの苦労なんだったの…
あとはlayerやbackgroundが不要だったりなんだりで、削除すると2/3くらいにはなる。
Method Drawはsvg-editのforkなわけだが、かなりUIに改善が入っている。ただAuthorが飽きたのかずっと放置されており、バグもごろごろ残っているので注意。
Method Drawの良いところは、数値での座標・幅・高さ指定が非常に楽で、またすべての座標を数値指定することで、座標が整数にきっちり揃ったSVGが手に入る。これは座標計算をJS側でやる際に都合が良い。
完成品のコードは以下。
ポイント
Vue.js + SVG練習3 : こっそり戻ろうとするスライダー
3作目。こっそり戻るスライダーを実装しよう。
素のSVG手書きするのちょっと辛すぎたんでInkscapeを使うことにした。 Inkscapeも辛くないということはまったくないけど…、うーん。Sketchの方が楽かな。Mac版は持ってるんだけど。
こんな感じ。
で、動かしたい単位でグループ化し、地道に名前を付けていく。
groupの移動はx, yではなくtranslateでやることになる(めんどくさい!)ので、 この時、translateの値は、変位する値でメモしておく。
始点:translate(-0.28348214,0.09449405) 終点:translate(89.296875,0.09449405)
なるほど、この範囲を動かせばいいわけだ。
inkscapeの安定版はいつでもこんな感じ。
まぁ使えるのでいいとする。 で、そのままSVGで出力すると、inkscape独自属性とかが山盛りで出力されてしまうので、プレーンSVGというので出す。 最適化SVGというのもあったけど、エラーになって出せなかった(そんなもんだ)
使う部分だけ切り出す。
metadataとかは不要かな。defもいらない。xml定義周りもいいや。 svg要素以下を.vueファイルにペーストするとこんな感じに。
こーいうところを
<g transform="translate(-0.28348214,0.09449405)" id="chara">
こうする。
<g :transform="charaPosition" id="chara">
computed:{ charaPosition(){ const x = -0.28348214 + 89.296875 * this.val / 100 return `translate(${x},0.09449405)` } }
これで、this.valが0-100の間を取ったときに動くようになる。
(追記)あとあと見てみると、ここの計算は間違っている。最終的にはここ消したんだけど
あとはドラッグ。面倒なものに着手してしまったことをだんだん後悔してきたぞ。
で、ここまでやったんですけど、yak shavingに遭遇したので、説明を放棄します。
- layer全体にtranslateがかかっていて、e.offsetX, e.offsetYの値がズレる
- 上記を解決しても、viewBoxの値が謎の値になっていて、e.offsetX, e.offsetYの値がズレる
- 上記を修正するために、viewBoxとsvgのwidth, heightの値をあわせ、オブジェクトをリサイズした所、移動にtranslateではなくmatrixが使われてしまう。リサイズしたらそうなるのか…座標計算で考えることが増えそう…という感じでどんどん不安になる
感想としては、見た目の辻褄があっていればOK、という感じのSVGが作られている。パーツ個々はともかく、UI全体をInkscapeから吐いたSVGで作るのはつらそう。
何かの機能を使わないように気をつけて作るとか、あるいは何らかの手段でSVGをclean up出来る、などの対処方法は存在するのかもしれない。Inkscapeマスター誰か…
完成品。
Slider.vue
ポイント
- 自分でこの手のドラッグものを作るときは、下記のものを実装している。
- startDrag, onDrag, stopDragの3種のイベント。onDragはグラブする要素に。startDragとstopDragはラッパー要素に付ける。
- offsetとdragの2つの状態。offsetは「グラブ要素の掴んだ位置」で、今回はx座標のみにして手抜きしてるけど、通常x, y座標を持たせる。dragも今回は2値だけど、リサイズとかある場合には複数の状態を取れるようにする。
- もはやtouch eventに付き合ってられないなと思うので躊躇なくpointer eventを使う。メジャーなライブラリは大抵touch eventとmouse eventの変換処理を地道にやってるのでこういう仕事の人はがんばるしかない
- scaleとinvertの2種類の変換関数
- 今回は雑に書いたけど、算数が苦手な自分のようなプログラマーはd3-scaleを利用するのが良いと思う。
疲れた、次はもっと簡単なやつにしよう。
ちょっとVue component試作したいときにpoiを使う
Vue.jsは「vue-cli」という便利なCLIツールがあって、下記のコマンドでvueファイルの単体起動をすることが出来た。
vue build MyComponent.vue
で、この機能なんだけど、いつの間にか削除されていたようだ。
もともとegoistさんという方がvbuildというツールを作っていて、その機能がvue-cliに取り込まれたという経緯だったかと記憶してる(その辺曖昧なので間違ってるかも)。
で、poiの方はegoistさんのプロジェクトなので、vue-cliになんでも盛り込むのではなく、興味があってゴリゴリ開発する人に移譲するというのはいい方向だとは思う。poiはVue専用でもなくてReactとかにも対応している(ElectronやPWAなんてのもある。ただこのあたりはボイラープレート戦国時代みたいに群雄割拠しているので、自分も何を使ったらいいのか全くわからない)。
で、SVGでVueコンポーネント作るのに、webpack-simpleテンプレート使っていたけど、どうせwebpack.config.jsいじりたくはないし、poiを使ってスリムダウンすることにする。
移行前
移行後
webpack.config.jsと.babelrcが消えた。
babelやwebpackはpoiのデフォルト設定で稼働するので、設定が気に食わなければ、poi.config.jsや.babelrcの直置きで変更することになる。
デプロイも簡単で、poi build
するとdist/
以下に下記のようにminifyしたファイルが吐かれる。
なお、poi MyComponent.vue
みたいなのは通らない。下記のindex.jsを用意してあげる必要がある。
index.js
import Vue from 'vue' import App from './components/App.vue' new Vue({ el: '#app', render: h => h(App) })
このくらいなら、別に用意するのは苦ではない。
あと、poiを使ってコンポーネント集作れそうなボイラープレートを作っておいた。 もし、まかり間違ってelement-uiのようなガッツリしたコンポーネント集を作る案件に遭遇してしまったら、使えるかもしれない。