SVGで図形を書いてみよう

練習にイチからSVGを書いてみようと思いました。

use を使った物を作りたいので、同じ図形が複数出てくるもの...五輪!

今にぴったりなので作ってみることにしました。

著作権の問題でNGでした

もしやと思って調べたところ、

オリンピック・パラリンピックに関する知的財産等の無断使用および不正使用ないし流用は法的にも罰せられます。

オリンピック・パラリンピックに関するエンブレム、ロゴ、用語、名称をはじめとする知的財産は、日本国内では「商標法」、「不正競争防止法」、「著作権法」等により保護されています。

フリー素材でロゴ画像が手に入るぐらいなので大丈夫そうですが、こわいのでやめておきます。
(すでに登録していた五輪画像が記事のサムネイルになってしまっている。どうしたらいいのでしょう)

他の画像を探さなくては。

目標の画像

オリンピック開催中に作ろうという期限もなくなったので、せっかくならいろいろな要素を盛り込んだものを作りたいと思い、下記イラレで作った画像を目指します。

f:id:yokoyoko_115:20210807012044j:plain

The 子供の絵!
子供の絵にはお日様が右上か左上によく描かれていますよね。太陽の存在感はすごい。

ポイントは

  • 虹:グラデーションをかける
  • 雲:useを使う、拡大縮小
  • 太陽:光の線でuseを使う

です。

あわよくば太陽の光の線が回転して、雲が右から左に動かせたらいいなと思います。

作ってみました

動かす前と動かしたあとでコードが若干変わりました。以下は動かしています。

See the Pen GORIN by yokoyoko (@yokoyoko_code) on CodePen.

コードについて

今回は虹・雲・太陽をそれぞれのSVGの図形として、それらをまとめて1つのSVG画像としています。

1つずつコードを整えていきます。

イラレでうまく作れず、ドーナツの半分の形にしたところ、グラデーションが崩れてしまいました。
パスだけ使って、グラデーションはsvgファイルを直接触ろうと思います。

イラレから書き出した虹SVG

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 380.46 190">
  <defs>
    <style>.cls-1{fill:url(#名称未設定グラデーション_350);}</style>
    <radialGradient id="名称未設定グラデーション_350" cx="190.23" cy="95" r="150.35" gradientUnits="userSpaceOnUse">
      <stop offset="0.7" stop-color="#c7659a"/>
      <stop offset="0.76" stop-color="#51527d"/>
      <stop offset="0.82" stop-color="#4facd2"/>
      <stop offset="0.88" stop-color="#369263"/>
      <stop offset="0.94" stop-color="#f4eb5f"/>
      <stop offset="1" stop-color="#d94f4f"/>
    </radialGradient>
  </defs>
  <g id="レイヤー_2" data-name="レイヤー 2">
    <g id="レイヤー_1-2" data-name="レイヤー 1">
    <path class="cls-1" d="M55.69,190a134.54,134.54,0,0,1,269.08,0h55.69C380.33,85,295.21,0,190.23,0S.13,85,0,190Z"/>
    </g>
  </g>
</svg>

path が虹の輪郭で、style で塗りつぶし(fill)をしています。
fill プロパティでurlを radialGradient に紐づけています。
そして radialGradientdefs で囲まれて定義され、見えなくなっています。

fill属性を使う

fill はプロパティと属性、どちらもあります。

この場合 style を使って path にfillプロパティをあてていますが、直接 path にfill属性を入れてもできます。

<path fill="url(#rainbow)" d="M55.69,190a134.54,134.54,0,0,1,269.08,0h55.69C380.33,85,295.21,0,190.23,0S.13,85,0,190Z"/>

これで style は削除できます。

radialGradient要素の設定

radialGradient は放射状のグラデーションを定義します。 cx、cy属性は、viewBoxのx、y座標を示し、r属性は半径を示します。

半円ドーナツ図形の中心がグラデーションの起点になってしまっていたので、xの座標は中央のままで、yの座標を図形の下辺にし、半径も半円ドーナツ図形をぴったり囲むように大きくします。

f:id:yokoyoko_115:20210808003235j:plain

ちなみに gradientUnits="userSpaceOnUse" についても調べましたが、分かりませんでした。

グラデーションとパターン – SVG 1.1 (第2版)

gradientUnits="userSpaceOnUse" の場合、 cx, cy, r, fx, fy は,グラデーション要素が参照された所における現在の利用座標系(すなわち fill あるいは stroke プロパティによりグラデーション要素を参照している要素の利用座標系)に対し, gradientTransform 属性で指定される変換を適用した結果の座標系における値を表す。

これを書かない、または gradientUnits="objectBoundingBox" (初期値)だと、虹が真っ赤になってしまったので今回の場合は必要です。

gradientTransform属性を知る時が来れば分かるようになるかもしれません。

イラレから書き出した雲SVG

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 121 80.67">
  <defs>
    <style>.cls-1{fill:#fff;}</style>
  </defs>
  <g id="レイヤー_2" data-name="レイヤー 2">
    <g id="レイヤー_1-2" data-name="レイヤー 1">
      <path class="cls-1" d="M96.51,15.85a24.73,24.73,0,0,0-4.33.4A24.45,24.45,0,0,0,51.61,7.41a24.4,24.4,0,0,0-38.27,24.1A24.48,24.48,0,1,0,44.5,67.38a24.47,24.47,0,0,0,45-3.58,24.48,24.48,0,1,0,7-47.95Z"/>
    </g>
  </g>
</svg>

雲は2つ出てくるのでuseを使ってみます。

viewBox,width,heightの値で雲の大きさ、x,yの値で雲の表示位置を設定する

1つのSVG画像として表示するにあたり、虹や雲など個別で書き出したSVG図形をどう配置するか混乱しましたが、MDNのページを見てこう考えました。

<symbol> - SVG: Scalable Vector Graphics | MDN

  1. まず定義側のviewBoxを考える。すでに書き出されたpathの座標(d)があるのでここはむやみに書き換えられない。viewBoxは図形いっぱいの枠と考える
  2. 定義側のwidth、heightを考える。これらの値に図形が拡大縮小する。そしてこのwidth、heightの値が呼び出し側の単位と合う
  3. 呼び出し側の use にwidth、height属性で大きさ、x、y属性で座標を指定して配置する

図にするとこういうイメージです。

f:id:yokoyoko_115:20210809014420j:plain

もし symbol にviewBox属性を指定していないと、たとえ symbol にwidth、height属性を指定していても use でwidth、height属性を指定して拡大・縮小できませんでした。もとの大きさのままです。
symbol にviewBox属性は必須

もしsymbol にwidth、height属性を指定していない場合、use でwidth、height属性を指定していれば拡大・縮小できます。
このとき use でwidth、height属性を指定していなければ呼び出し側のviewBoxの大きさいっぱいになります。
symbol または use でwidth、height属性を指定する

<use> - SVG: Scalable Vector Graphics | MDN

use 要素にあるx, y, width (en-US), height (en-US), href の各属性のみが参照される要素に設定されているものを上書きします。

複数の図形を拡大・縮小させて配置するなら use でwidth、height属性を指定することになりそうです。

ちなみに、widthかheight片方のみを指定した場合(もう片方が未指定)、widthかheightの初期値は呼び出し側のviewBoxの大きさになりますが、おそらくpreserveAspectRatioの値がデフォルトであることが原因で、呼び出した図形はviewBoxの範囲内で上下左右中央に配置されます。

x、y属性でうまく配置するには、width、height属性で図形ぴったりの比率で指定する必要がありそうです。

先ほどの虹も結局 use を使うことになりました。

太陽

最後は太陽です。

イラレから書き出した太陽SVG

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115.39 115.39">
  <defs>
    <style>.cls-1{fill:#f08218;}.cls-2{fill:none;stroke:#f08218;stroke-miterlimit:10;stroke-width:4px;}</style>
  </defs>
  <g id="レイヤー_2" data-name="レイヤー 2">
    <g id="レイヤー_1-2" data-name="レイヤー 1">
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="15.94" y1="57.7" y2="57.7"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="28.17" y1="87.22" x2="16.9" y2="98.49"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="57.7" y1="99.45" x2="57.7" y2="115.39"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="87.22" y1="87.22" x2="98.49" y2="98.49"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="99.45" y1="57.7" x2="115.39" y2="57.7"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="87.22" y1="28.17" x2="98.49" y2="16.9"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="57.7" y1="15.94" x2="57.7"/>
      <circle class="cls-1" cx="57.7" cy="57.7" r="30.28"/>
      <line class="cls-2" x1="28.17" y1="28.17" x2="16.9" y2="16.9"/>
    </g>
  </g>
</svg>

line は複数あるのは分かりますが何故か circle も同じ数だけあります!
各属性値も同じなので、8つの円が重なっているようです。7つは消します。

この line のx1、y1とx2、y2属性、1が始点で2が終点のx、y座標をあらわしています。
指定されていない場合、初期値は0です。

8つある line のうち1つを使って、太陽の縁を中心に45度ずつ回転させて表示させてみよう!

...と当初は思ったのですが、use で太陽の光の線を8回呼び出すのはどうなのか、それだと後々の「光の線を回転させる」が難しいのではと思い、まとめて呼び出すことにしました。

symbolを呼び出すのかgを呼び出すのか

太陽の図形は「円」と「光の線」で分けて出したいので、ざっくりと下記のようにしました。

定義側の太陽

    <symbol>
      <g>
        <circle>
      </g>
      <g>
        <line>
           :  (✕ 7)
      </g>
    </symbol> 

symbolg はid属性を振れば use で呼び出せます。
この場合は太陽まるごと呼び出すときは symbol 、「円」と「光の線」で2回呼び出すときは g を使ってできます。

2つは呼び出したときのhtmlへの出力が異なります。

symbol の場合は、symbolsvg に置き換わった形で出力されます。 中で g に「円」と「光の線」がそれぞれ囲まれています。 一方 g の場合はそのまま出力されます。

なので「円」と「光の線」で分けられていながら、1つの図形としてまとまっている symbol で呼び出すことにしました。

2つのg要素をさらにg要素で囲い、それを呼び出す方法もありますが、g要素は呼び出し側でwidthとheightの指定ができません。

symbolg の使い分けは、いろいろ作っていくうちに慣れてきそうな気がしました。
迷ったら symbol で囲います。

classとスタイルのあて方

pathguse など様々な要素でスタイルをあてることができるので、何がベストなのか大混乱です。

大事なのは定義側で指定したスタイルを呼び出し側で上書きできないということです。

雲のところで大きさの上書き指定は可能だがその他はできない、というのはこの事だったのですね!

定義側の pathfill: red とした場合、呼び出し側の usefill: blue と指定しても青にはなりません。

そう考えると、呼び出し側に統一してスタイルをあてた方が良いのでしょうか。
先の「symbolを呼び出すのかgを呼び出すのか」問題とも関係してくるので、状況によりけりなのかなと思いました。

ちなみにデベロッパーツールでスタイルを確認したときにつまづきました。定義側と呼び出し側、それぞれを確認しないといけないです。

定義側のsvgのスペースを削除する

SVG画像の前に、定義したsvg要素に謎の300x150pxの隙間ができました。

svgdisplay="none" を書いたり、csssvg { display: none; } を指定するとうまくいくと思ったのですが、
なんと虹が消えてしまいました。

虹で使っている radialGradientdisplay: none で無効になってしまったのだと思います。

そこで色々と試行錯誤したところ、

/* 定義側のsvg */
.svg-defs {
  display: block;
  width: 0;
  height: 0;
}

としたところ、隙間が消えてくれました。

他のケースでも使えるといいなと思います。

できあがったSVG(動かない)

これで虹・雲・太陽がそろいました!
大きさや位置が目分量ですが真似できました◎

HTML

<!-- 定義側 -->
<svg class="svg-defs">
  <defs>
    <!-- 虹 -->
    <symbol id="rainbow" viewBox="0 0 380.46 190">
      <radialGradient id="rainbow-gra" cx="190.23" cy="190" r="190.23" gradientUnits="userSpaceOnUse">
        <stop offset="0.7" stop-color="#c7659a"/>
        <stop offset="0.76" stop-color="#51527d"/>
        <stop offset="0.82" stop-color="#4facd2"/>
        <stop offset="0.88" stop-color="#369263"/>
        <stop offset="0.94" stop-color="#f4eb5f"/>
        <stop offset="1" stop-color="#d94f4f"/>
      </radialGradient>
      <path fill="url(#rainbow-gra)" d="M55.69,190a134.54,134.54,0,0,1,269.08,0h55.69C380.33,85,295.21,0,190.23,0S.13,85,0,190Z"/>
    </symbol>
    <!-- 雲 -->
    <symbol id="cloud" viewBox="0 0 121 80.67">
      <path d="M96.51,15.85a24.73,24.73,0,0,0-4.33.4A24.45,24.45,0,0,0,51.61,7.41a24.4,24.4,0,0,0-38.27,24.1A24.48,24.48,0,1,0,44.5,67.38a24.47,24.47,0,0,0,45-3.58,24.48,24.48,0,1,0,7-47.95Z"/>
    </symbol>
    <!-- 太陽 -->
    <symbol id="sun" viewBox="0 0 115.39 115.39">
      <g id="sun-circle">
        <circle class="sun-circle" cx="57.7" cy="57.7" r="30.28"/>
      </g>
      <g id="sun-line" class="sun-line">
        <line x1="15.94" y1="57.7" y2="57.7"/>
        <line x1="28.17" y1="87.22" x2="16.9" y2="98.49"/>
        <line x1="57.7" y1="99.45" x2="57.7" y2="115.39"/>
        <line x1="87.22" y1="87.22" x2="98.49" y2="98.49"/>
        <line x1="99.45" y1="57.7" x2="115.39" y2="57.7"/>
        <line x1="87.22" y1="28.17" x2="98.49" y2="16.9"/>
        <line x1="57.7" y1="15.94" x2="57.7"/>
        <line x1="28.17" y1="28.17" x2="16.9" y2="16.9"/>
      </g>
    </symbol>    
  </defs>
</svg>

<!-- 呼び出し側 -->
<svg  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="oekaki-area" width="500" height="300" viewBox="0 0 500 300" role="img" aria-labelledby="oekaki">
<!-- 雲 -->
  <use href="#cloud" class="cloud" x="250" y="10" width="140" height="93.3"></use>
  <use href="#cloud" class="cloud" x="350" y="160" width="120" height="80"></use>
<!-- 虹 -->
  <use href="#rainbow" x="75" y="125"  width="350" height="175"></use>
<!-- 太陽 -->
  <use href="#sun" x="20" y="20" width="130" height="130"></use> 
<!-- 太陽のパーツを個別に出す場合 -->
<!--   <use class="sun-circle" href="#sun-circle" x="20" y="20" width="130" height="130"></use> 
  <use class="sun-line" href="#sun-line" x="20" y="20" width="130" height="130"></use>
</svg> -->

CSS

.svg-defs {
  display: block;
  width: 0;
  height: 0;
}

#oekaki-area {
  border: 1px solid #000;
  background-color: #def0f3;
}
.cloud {
  fill: #fff;
}
.sun-circle {
  fill: #f08218;
}
.sun-line {
  stroke: #f08218;
  stroke-miterlimit: 10;
  stroke-width: 4px;
}

雲を動かす

さらにSVGの図形に動きを加えてみます。

雲は静止画の位置から左へ移動して、また右から現れるようにします。

こちらを見ながら書いたらばっちりだと思います。

SVG アニメーション(SMIL を使ったアニメーション) / Web Design Leaves

せっかくならSVGの中で動かしたい...!

SMIL(Synchronized Multimedia Integration Language)というマークアップ言語を使う方法がそれにあたるようです。

SVG animation with SMIL - SVG: Scalable Vector Graphics | MDN

SMIL では以下のようなことができます: ・要素の数値属性 (x, y など) のアニメーション ・トランスフォーム属性 (translation または rotation) のアニメーション ・色属性のアニメーション ・モーションパスに従う

具体的には animateMotionanimate など、アニメーションに関係するSVG要素を書きます。
cssを書かずにアニメーションできます。

animateMotion、animate要素が使えるかどうか

はじめは animateMotion を使うのが適当と思いましたが、pathに沿った動きをさせたい場合に使うようです。

ふわふわ上下しながら流れていって欲しいのが正直なところですが、ここは右から左に水平に動かします。

続いて animate を考えましたが、こちらは要素の属性のアニメーションに使うものなので、雲の path には位置に関わる属性(x,y)がなくて動かせない。
雲を変形させることはできます。

cx,cy属性のある circle であれば animate を使って移動のアニメーションができます。

呼び出し側で使えない。定義側も問題あり

animate などのアニメーション要素は use と絡めて書くことはできないのではと思いました。

アニメーション要素は、 path など形を表す要素の子要素になるように書きます。
<path><animate></path> です。
path で囲む書き方もできるのですね。

use を使うと svg で書き出されるので、animate を差し込む余地がありません。

また、定義側でアニメーションの定義をすればよいのではと考えましたが、雲の定義の仕方では無理そうです。 定義側では雲の形状だけを設定していて、viewBoxが雲いっぱいのサイズのため雲が移動するスペースがありません。

呼び出し側と同じviewBoxで、定義側で雲が配置されていれば可能なのかもしれませんが、まだ今の知識では書けません。

となると、雲の場合はcssに頼るしかないのかもしれません。

cssのanimationプロパティで動かす

静止画の位置から右から左に移動させる、というところで少しつまずきました。

0%→100%で右端(見えない)→左端(見えない)とすると、最初の表示のタイミングでは雲が見えません。

その場合は animation-delay プロパティにマイナスの値を設定するとうまくいきます。
遅らすのではなく早めることができるので、0%→100%の途中からスタートできます。

10秒かけた移動だから...とこれも目分量で設定しました。

雲は動きました!

太陽の光を動かす

こちらはSMIL で動かすことができました。

animateTransform要素で回転させる

参考ページなどを見てすんなりとできました。

g で囲った中で animateTransform を書いて g 内の要素の動きを設定できます。

ちなみにデフォルトでは回転は時計回り(右回り)ですが、to="-360, ..." とマイナスの値だと左回りになりました。

さいごに

当初の五輪だったらここまで長くならなかったですね。
SVGはまだまだ奥深いことがわかりました。

※検証段階の内容が多分に含まれているので、分かり次第修正していきます。

順番が前後してしまいますが、この後SVGについてまとめた記事を書きます。

実践が大事!おしまい!