気づいて築く

ほぼ気づかないフリ

2017年12月16日 土曜日

Searchlight Effect Using SVG

これはSVG Advent Calendar 2017の16日目の記事です。

サーチライト(スポットライト)を当てたような演出をSVGで実現する方法をご紹介します。
具体的にはこんな演出のことです。
backdrop

DOMとCSSで実現するとしたら

Intro.jsというJavaScriptのライブラリをご存知でしょうか。これはWebサイトのチュートリアルガイドを作成するためのものですが、スポットライトっぽい演出が使われています。
このような演出をDOMとCSSで実現しようとした場合、全体を暗くすることは簡単ですが、ライトが当たっているところ(明るいところ)をどうやって「くり抜く」かが問題になります。ブラウザのDevelopr Toolsなどで確認すると、Intro.jsでは対象エレメントのz-indexを極端に大きな値(9999999)にすることで無理やり最前面に引っ張り出しているのが分かります。
Intro.js なお、CSSのmaskプロパティを使えばもっとスマートに実現できるかもしれません。ただ今回はSVGがテーマなので検証はしていません。

SVGで実現するとしたら

SVGでサーチライトの演出を実現する方法はふたつあります。ひとつはmaskを使う方法、もうひとつはpathのfill-rule属性を応用する方法です。前者の方がより直感的で分かりやすいですが、後者の方がパフォーマンスが良い(動作が軽い)です。
とりあえずmaskを使う方法を順に見ていくことにします。
まず、SVGで四角形を描画します。

<svg viewBox="0 0 400 300">
  <rect fill="blue" x="0" y="0" width="400" height="300"></rect>
</svg>

次に、この四角形の上に同サイズの白い四角形と、黒い円を描画します。(白い四角形には見やすくするため枠線を引いていますが本来は不要です)。

<svg viewBox="0 0 400 300">
  <rect fill="blue" x="0" y="0" width="400" height="300"></rect>

  <rect fill="white" stroke="black" x="0" y="0" width="400" height="300"></rect>
  <circle fill="black" cx="200" cy="150" r="100"></circle>
</svg>

これでは青い四角形が完全に覆い隠されてしまっていますので、maskを使って黒い円の部分をくり抜ぬいてみます。

<svg viewBox="0 0 400 300">
  <!-- maskの適用 -->
  <rect mask="url(#mask)" fill="blue" x="0" y="0" width="400" height="300"></rect>

  <!-- maskの作成 -->
  <mask id="mask">
    <rect fill="white" x="0" y="0" width="400" height="300"></rect>
    <circle fill="black" cx="200" cy="150" r="100"></circle>
  </mask>
</svg>

maskは白い部分は透過し黒い部分はくり抜きます。従って、青い四角形の真ん中にポッカリ穴を開けることができるというわけです。

グラデーションとぼかし

上の結果では全然サーチライトに見えません。グラデーションとぼかしの機能を使ってそれっぽく仕上げてみましょう。
まずはグラデーションを使ってそれっぽい影を描画してみます。

<svg viewBox="0 0 400 300">
  <!-- maskとグラデーションの適用 -->
  <rect mask="url(#mask)" fill="url(#gradient)" x="0" y="0" width="400" height="300"></rect>

  <!-- maskの作成 -->
  <mask id="mask">
    <rect fill="white" x="0" y="0" width="400" height="300"></rect>
    <circle fill="black" cx="200" cy="150" r="100"></circle>
  </mask>

  <!-- グラデーションの作成 -->
  <defs>
    <radialGradient id="gradient">
      <stop offset="0%" stop-color="black" stop-opacity="0.4"></stop>
      <stop offset="100%" stop-color="black" stop-opacity="0.9"></stop>
    </radialGradient>
  </defs>
</svg>

かなりそれっぽくなりました。更にそれっぽくするために、ライトが当たっている部分の境界をぼかしてみます。

<svg viewBox="0 0 400 300">
  <!-- maskとグラデーションの適用 -->
  <rect mask="url(#mask)" fill="url(#gradient)" x="0" y="0" width="400" height="300"></rect>

  <!-- maskの作成 -->
  <mask id="mask">
    <rect fill="white" x="0" y="0" width="400" height="300"></rect>
    <!-- ぼかしの適用 -->
    <circle filter="url(#filter)" fill="black" cx="200" cy="150" r="100"></circle>

    <!-- ぼかしの作成 -->
    <defs>
      <filter id="filter">
        <feGaussianBlur in="SourceAlpha" stdDeviation="2"></feGaussianBlur>
      </filter>
    </defs>
  </mask>

  <!-- グラデーションの作成 -->
  <defs>
    <radialGradient id="gradient">
      <stop offset="0%" stop-color="black" stop-opacity="0.4"></stop>
      <stop offset="100%" stop-color="black" stop-opacity="0.9"></stop>
    </radialGradient>
  </defs>
</svg>

これを画像に重ねると以下のようになります。

lupin
© モンキー・パンチ

サーチライトを動かす

冒頭で少し触れましたが、maskやぼかしといったエフェクトは重い処理なので、頻繁に再描画するときはなるべく避けたほうがパフォーマンスは向上します。これ以降はmaskの代わりにpathを使いますが、可読性が低いため少々分かりづらいかもしれません。
とりあえずmask処理で作ったものと同様のものをpathで作ってみます。

<svg viewBox="0 0 400 300">
  <!-- ぼかしとグラデーションの適用 -->
  <path fill-rule="evenodd" fill="url(#gradient)" filter="url(#filter)" d="M0,0 L400,0 L400,300 L0,300Z M200,50 A 100,100 0 1,1 200,250 A 100,100 0 1,1 200,50"></path>

  <defs>
    <!-- ぼかしの作成 -->
    <filter id="filter">
      <feGaussianBlur in="SourceAlpha" stdDeviation="2"></feGaussianBlur>
    </filter>

    <!-- グラデーションの作成 -->
    <radialGradient id="gradient">
      <stop offset="0%" stop-color="black" stop-opacity="0.4"></stop>
      <stop offset="100%" stop-color="black" stop-opacity="0.9"></stop>
    </radialGradient>
  </defs>
</svg>

pathのd属性がカオスな感じですが、要約すると、大きい四角形を描画して中に重ねて円を描画しています。このときfill-rule属性に「evenodd」という値を設定すると、重ねた円の部分がくり抜かれます。これがpathを使った方法です。
画像に重ねると以下のようになります。
サーチライトがマウスカーソルの動きに追従します。

lupin
© モンキー・パンチ

このようなアニメーションはmask処理を使った方法でも可能です。pathを使った方が処理が軽いというだけです。
具体的な実現方法の解説は省略しますが、ソースコードはこのページに直接埋まっているので興味があればご覧ください。

さて、これまではSVGを画像に重ねてきましたが、画面全体に重ね、各要素の座標とサイズを調べてそれに合わせてサーチライトの位置を書き換えてやれば、Intro.jsっぽいエフェクトを再現することができます。

あれや

これや

それや

どれや

なんやかんや

こちらも具体的な実現方法の解説は省略しますが、ソースコードはこのページに直接埋まっているので興味があればご覧ください。