material-uiのSVGアイコンを作る

Googleはマテリアルデザインのガイドラインを提供するほかに、CSS+ JavaScriptのライブラリ(Material Design Lite)やフォント(Roboto/Notoなど)も用意し、アイコンもまたまとまった数のものがあります。

design.google.com

このアイコンは最も使われるだろうPNG形式、ついでアイコンフォント形式のほか、SVG形式も一緒に配布されています。取り扱いとして他より工夫がいるけれども拡大縮小に強い上でHTML文書に直接埋め込めるSVGは、私は最近理解して使い始めたばかりですが、アプリケーションも構成しやすくコードで直接コントロールもできるので重宝し始めました。

承前としてSVG

<html>
<body>
<svg>
    <path d="M11.5 17l4-8v-2h-6v2h4l-4 8h2z"/>
</svg>
</body>
</html>

まず承前として上記のHTML断片は数字の「7」をSVGで書いてます。描画はsvg>path@dで設定されているコマンド文字列でベクタ描画。これの読み方が知らないと訳わかりませんが順を追っていくと結構単純なことの組み合わせでした。まず"M11.5 17l4-8v-2h-6v2h4l-4 8h2z"はアルファベット+数字の組み合わせで、区切り文字はカンマか空白か符号というルールで分解します。以下は独自ですが関数呼び出しのフォーマットで書き直してみます。

  • M(11.5, 17)
  • l(4, -8)
  • v(-2)
  • h(-6)
  • v(2)
  • h(4)
  • l(-4, 8)
  • h(2)
  • z

分解するとあとはアルファベットが命令、数字が座標です。命令アルファベットが大文字だと続く数字は絶対座標で、小文字だと相対座標になります。Mは始点の指定コマンドのためまず(11.5, 17)から続くl(小文字のエル)コマンドで相対座標(+4, -8)へ直線を引きます。vは垂直線、hは水平線ですから、vで相対座標(0, -2)、hで相対座標(-6, 0)へ。。。以下続く。最後にzはパスを閉じるコマンドで、ここで始めにMで決めた絶対座標(11.5, 17)へ戻ってきてなければいけません。他、ベジェ曲線を書くこともできます。

Paths - SVG | MDN

material-ui

materilal-uiでは、このSVGを保持するReactコンポーネントがあり、Material Iconsをそのまま取り込んでくれています。

'use strict';
import React from 'react';
import {ToggleStar, ToggleStarHalf, ToggleStarBorder} from 'material-ui/lib/svg-icons';

const Stars = React.createClass({
    render() {
        return (
            <div>
                <div><ToggleStarBorder/></div>
                <div><ToggleStarHalf/></div>
                <div><ToggleStar/></div>
            <div>
        );
    }
});
export default Stars; 

f:id:masataka_k:20160228064049p:plain

こんな感じ。このToggleStarとかは見るとSvgIconコンポーネントにMaterial Iconからそのままもらってきたpathデータを流し込んで作ってます。

SVGアイコン自作

ということで、さっきの7をコンポネント化します。そもそも7を作り8も9も作った理由はMaterial IconでImage-Looksという角丸の四角に素朴な味わいの数字が入ったアイコンが1から6までしかなかったからでした(looks one 〜 looks 6まで)。

'use strict';

import React from 'react';
import {SvgIcon} from 'material-ui';
const backgroundRoundRect = 'M19 3c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-14c-1.1 0-2-.9-2-2v-14c0-1.1.9-2 2-2h14z';
const seven = 'M11.5 17l4-8v-2h-6v2h4l-4 8h2z';
function Looks7(props) {
    return (
        <SvgIcon {...props}>
            <path d={backgroundRoundRect + seven}/>
        </SvgIcon>
    );
}

const eight = 'M11 17h2c1.1 0 2-.89 2-2v-1.5c0-.83-.67-1.5-1.5-1.5.83 0 1.5-.67 1.5-1.5v-1.5c0-1.11-.9-2-2-2h-2c-1.1 0-2 .89-2 2v1.5c0 .83.67 1.5 1.5 1.5-.83 0-1.5.67-1.5 1.5v1.5c0 1.11.9 2 2 2zM11 9h2v2h-2v-2zM11 13h2v2h-2v-2z';
function Looks8(props) {
    return (
        <SvgIcon {...props}>
            <path d={backgroundRoundRect + eight}/>
        </SvgIcon>
    );
}

const nine = 'M13 7h-2c-1.1 0-2 .89-2 2v2c0 1.11.9 2 2 2h2v2h-4v2h4c1.1 0 2-.89 2-2v-6c0-1.11-.9-2-2-2zm0 4h-2v-2h2v2z';
function Looks9(props) {
    return (
        <SvgIcon {...props}>
            <path d={backgroundRoundRect + nine}/>
        </SvgIcon>
    );
}

苦労した点は、Material Iconのデータが”汚い”コードだってことが一つ。多分自動生成した際のロジックが悪いのか、描画内で相対座標と絶対座標を雑に混在したものになっているために、既存アイコンのパスデータの一部を持ってきて新しいアイコンを作ろうとすると、すぐ座標系が壊れちゃう。そのためにパス要素毎にまず始めにMコマンドで絶対座標を決めてからそこからzで閉じるまでは相対パスで書き通すように変更して再利用しました。

苦労したもう一つは、7/8/9のアイコンを作るにあたって、背景の ”backgroundRoundRect” と "seven"/'eight"/"nine" をつなげて数字がくり抜きできなかったこと。苦労の結果、今は白抜きできています。こちらの原因はパスの書き方で、同じ図形でも時計回りにパスを辿っても反時計周りに辿っても見かけは一緒なのですが、同じ巻きを重ねてもくり抜かれないというルールがありました。よって数字は反時計まわりだったので背景は時計まわりにしなければならない。超はまった。

しかし、わかると最強。これは便利だ。