はじめに
前回はPNG形式のアイコンを操作する方法を紹介しました。ですが、PNGやJPGの様な画像形式のアイコンだと拡大した際にギザギザが目立って見栄えに課題がありました。 今回はこれを克服するベクトル画像形式のSVG(Scalable Vector Graphics)でアイコンを作り、それを操作するWebアプリをVue+Typescriptで作ってみたいと思います。
出来上がりはこんな感じ!
いつもどおり、最初に出来上がりのイメージを紹介します。ちょっとわかりづらいかもしれませんが、以下の様に拡大してもギザギザしないアイコンの表示と操作です。
コードはここです。 GitHub - Shikataramuno/svg-icon-sample: VueでSVG Icon
SVGアイコンの準備
さぁ!作りましょうっと行きたいところですが、前回同様にまずはSVGアイコンを準備するところからです。これまた、前回同様に長くなるので別記事にまとめました。どうぞお立ちよりください(😉)。
SVGアイコンの作り方はこちら
techblo.shikataramuno.com
これ以降は前回の記事で作ったSVGアイコンを使って、これまた前々回の記事で作成したPNG形式アイコンのコードをベースにSVGアイコンへの対応を進めていきます。
PNG形式アイコンの実装はこちら
techblo.shikataramuno.com
今回はVueの公式ページにある解説を参考にしています。
Vue公式ページの開設
jp.vuejs.org
SVGアイコンの埋め込み方
前置きが長くなりましたが、最初にSVGアイコンを表示させてみたいと思います。
PNG形式アイコンのコードのうち、App.vue
はそのまま使い、SampleIconView.vue
にSVGアイコン対応の変更を加えていきます。
まずは、公式ページの方法に従ってsrc/components
フォルダにIconBase.vue
を作成します。 次に、src/components
の下にicons
フォルダを作成し、そこにInkscape
で作ったアイコンのSVG形式のデータからアイコンを示すノードを抜き出してIconSmaple.vue
を作ります。
プロジェクトフォルダのツリー
src │ App.vue │ main.ts │ shims-tsx.d.ts │ shims-vue.d.ts ├─assets └─components │ IconBase.vue <--- これです。 │ SampleIconView.vue └─icons IconSample.vue <--- これです。
IconBase.vueはこんな感じです。
<template> <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 18 18" :aria-labelledby="iconName" role="presentation"> <title :id="iconName" lang="en">{{iconName}} icon</title> <g :fill="iconColor" :stroke="iconColor"> <slot /> </g> </svg> </template> <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class IconBase extends Vue { @Prop({default: 'box'}) iconName!: string; @Prop({default: 18}) width!: number | string; @Prop({default: 18}) height!: number | string; @Prop({default: 'currentColor'}) iconColor!: string; } </script>
15行目から18行目で親のコンポ―ネントから受け取るプロパティ(アイコン名、高さ、幅、色)を宣言しています。
4行目でアイコンを表示するviewBox
のサイズを指定し、これらのプロパティをsvgコンポーネントに渡しています。
5行目でslot
タグを使って親コンポーネント側で指定するsvgコンポーネントのコンテンツをそのまま埋め込んでいます。
IconSample.vueはこんな感じ
<template> <g> <rect ry="1" rx="1" y="0.75" x="0.75" height="16.5" width="16.5" id="rect4136" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle r="6" cy="9" cx="9" id="path4139" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle r="3" cy="9" cx="9" id="path4141" style="opacity:1;fill-opacity:1;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> </g> </template>
Inkscapeで作ったアイコンのデータそのままですね。
そしてIconBase.vue
とIconSample.vue
の各コンポーネントをSampleIconView.vue
のhtmlテンプレートに加えます。
こんな感じです。
<template> <div class="sample-icon-field"> <div class="sample-icon-field"> <icon-base icon-color="#ff0000" width=30 height=30 icon-name="sample"><icon-sample/></icon-base> <div class="message">{{message}}</div> </div> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import IconBase from './IconBase.vue'; import IconSample from './icons/IconSample.vue'; @Component({ components: { IconBase, IconSample, }, }) export default class SampleIconView extends Vue { name: string = 'SampleIconView'; message: string = ' ↑ クリック^2 !'; }
12行目~20行目でIconBase.vue
とIconSample.vue
のコンポーネントを読み込んで、4行目でIconBase
とIconSample
をテンプレートに加えています。
4行目のIconBase
のプロパティを使って色(icon-color
)や高さ(height
)、幅(width
)を変えてみてください。変化に応じてアイコンの大きさや色が変わると思います。とても便利ですね
ここまでで、Inkscapeで作った自前のSVGアイコンを表示することができます。これだけ?
そうです。案外簡単なんです!
クリックベントの補足と伝搬
次にPNGアイコンと同じ様にクリックでメッセージを変えてみましょう。
まずはアイコンにクリックイベントとそのハンドラーを登録しましょう。IconSample.vue
でsvgノードのプロパティにクリックイベントのハンドラを登録し、script
タグを追加してハンドラの実態を登録します。
IconSmaple.vue
でイベントを補足して親コンポーネント(SampeIconView.vue
)でイベントを処理する様にしてみます。
IconSample.vueはこんな感じです。
<template> <g> <rect v-on:click="clicked" ry="1" rx="1" y="0.75" x="0.75" height="16.5" width="16.5" id="rect4136" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle v-on:click="clicked" r="6" cy="9" cx="9" id="path4139" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle v-on:click="clicked" r="3" cy="9" cx="9" id="path4141" style="opacity:1;fill-opacity:1;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> </g> </template> <script lang="ts"> import { Component, Emit, Vue } from 'vue-property-decorator'; @Component export default class IconSample extends Vue { @Emit('clicked') clicked(): void { console.log('icon-sample clicked'); } } </script>
3,5,9行目で各SVGのノードにv-on:click="clicked"
でクリックイベントを定義し、ハンドラを登録しています。そして15~26行目でscript
タグを追加してクリックイベントのハンドラを定義し、親コンポーネントにemit
でイベントを伝搬させています。
親コンポーネントのSampleIconView.vue
はこんな感じです。
<template> <div class="sample-icon-field"> <icon-base icon-color="#ff0000" width=30 height=30 icon-name="sample"><icon-sample @clicked="iconClicked"/></icon-base> <div class="message">{{message}}</div> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import IconBase from './IconBase.vue'; import IconSample from './icons/IconSample.vue'; @Component({ components: { IconBase, IconSample, }, }) export default class SampleIconView extends Vue { name: string = 'SampleIconView'; message: string = ' ↑ クリック^2 !'; clickCount: number = 0; iconClicked(): void { alert('アイコンがクリックされました!'); this.clickCount++; this.message = 'アイコンが ' + this.clickCount + '回 クリックされました!'; } } </script>
3行目でアイコンのクリックイベントの登録とハンドラの定義を行い、22~27行目でイベントハンドラを実装しています。 これで、SVGアイコンでクリックイベントを検知して処理できるようになりました。
クリックしても…
もうお気づきかもしれませんが、このままでは、クリックする場所によってイベントが発火したりしなかったりします。試しにアイコンの隙間(外枠の線、中の円と丸の間の領域)をクリックしてみてください。何も起こりません。
理由は簡単で、SVGノードにクリックイベントを登録しているからです。SVGノードで定義していない領域ではクリックイベントが検知されません。UX的にちょっと問題ありですね。
では改善してみましょう。っといってもいたって簡単です。viewBox全域の最前面に透明な矩形のノードを定義してそこでイベントを検知するようにすればOKです。改善後のIconSample.vueは以下のような感じになります。
<template> <g> <rect ry="1" rx="1" y="0.75" x="0.75" height="16.5" width="16.5" id="rect4136" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle r="6" cy="9" cx="9" id="path4139" style="opacity:1;fill:none;fill-opacity:1;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <circle r="3" cy="9" cx="9" id="path4141" style="opacity:1;fill-opacity:1;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> <rect v-on:click="clicked" ry="1" rx="1" y="0" x="0" height="18" width="18" id="rect4145" style="opacity:1;fill-opacity:0;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> </g> </template>
12~14行目に新たに最前面にSVGノードを定義しています。形やサイズは一番上のrectノードと同じですが、fill
プロパティをなくしfill-opacity:0
を追加しています。これでアイコン領域の最前面に一番外枠の矩形と同じ領域で透明に塗りつぶされた矩形領域が定義できます。
後はこのノードでクリックイベントを検知するようにし、各ノードのイベント検知は削除します。これでアイコン領域のどこでもクリックイベントを処理できるようになります。
出来上がり!
いかがでしょうか?案外と簡単にSVGアイコンを表示し、操作できるといった印象ではないでしょうか? 後は如何に上手にSVGアイコンを描くか、その腕前とセンス次第といったところかな…
謝辞
今回を含め4本ほどの記事でVue +Typsescriptでアイコンを扱う方法や自前アイコンの作成方法など筆者のやり方を紹介させていただきました。 Webアプリ構築の際のお役に立てれば光栄です。最後まで読んでくださりありがとうございました。次回はまた別のテーマで記事を書きたいと思います。