2012年8月13日月曜日

svgフィルターのクロスブラウザ化手法

svgではfilter要素により元々の画像に様々な効果を与えることができます.これはこれで非常に魅力的な機能なのですが,一つ大きな壁が存在しています.それはブラウザ間の動作の違いです.

単純なフィルターであればそれほど問題は無いのですが,原始フィルターの掛け合わせが複雑になるに連れ,ブラウザの実装度合いにより動いたり動かなかったりと悩ましい問題が露見してきます.今回紹介するさざなみフィルターもアイディア自体はかなり前に思いついたものなのですが,クロスブラウザの問題から1ファイルで実現することが出来ず頓挫したものの一つです.

今回,ようやくその問題を解消することが出来たのでまとめて見ることとします.

さざなみフィルターは次のようなアイディアから構成されています.
  • 波のようなグラデーションを作ってアニメーションさせてみよう
  • そのグラデーションをフィルタの入力として任意の画像にさざなみのような効果をつけよう.
つまり,フィルターを介して2つの画像を合成しようといったものです.画像の合成自体は一般的な処理ですからできて当たり前なのですが,実際に試してみるとこの単純なことすらクロスブラウザで実現するのが困難であることが判ります.

通常filterに画像を与える方法としては次の3つの方法があります.
  1. 図形・画像要素にfilter属性を指定し,SourceGraphicを取得する.
  2. svg要素にenable-background=newを指定し,filter要素からBackgroundImageを取得する.
  3. feImage要素を使って直接画像を取得する.
が,この部分にクロスブラウザの問題が隠れています.
  • 1の画像入力は通常単一の図形要素のみが取得可能です.これはどのブラウザでも動作します.
  • 2はoperaでしか動作しません.
  • 3は通常任意の画像ファイルと図形要素を参照することができるはずなのですが,firefoxでは図形要素の参照がバグで無効になっています.またoperaでは外部の画像を参照した際にアニメーションが停止してしまうといった現象が出ます.
従って以前はfirefox用とchrome/opera用の2つのsvgに分けていたのですが,これでは非常に使いにくい形です.よって何とか1つのファイルにまとめる方法はないのか?が本質的な課題となりました.


この問題は無理に単一のfilterで凌ぐのではなく,filter処理を2つに分け,switch要素でブラウザ毎にfilterを切り替えることで解決しました.

svgではswitch要素というブラウザでのsvg対応状況に応じてグラフィックの内容を切り替えるための機構が備わっています.これは通常アニメーションに対応していない環境で静止画を適切に表示させるといった用途のために定められているのですが,現状ブラウザ毎にバラバラの処理結果となります.firefox,chrome,operaいずれもsvgアニメーションに対応しているにもかかわらず,firefoxでは「非対応」,それ以外では「対応」として扱われます.

つまり,この動作を逆手に取ればブラウザ毎に描画内容を切り替えることが可能となるのです.もちろんこの問題が解消されてしまえば元も子もないのですが.

従ってブラウザ毎に次のようにfilterを構成しました.
  • chrome/opera
    さざなみグラデーション画像をfeImageで取得し,それをfilterのSourceGraphicと合成した.
  • firefox
    さざなみグラデーション画像を同一のファイルから取得したいところだが,feImageが動作しないので,SourceGraphicから直接取得できるようにソース画像の隣に配置し,一括でfilterをかける.その後feOffsetでさざなみグラデーション画像を切り抜き,後はchrome/operaと同様に処理を行う.
※実はfirefoxにおいて複数画像を渡すのは結構面倒で,この解決策を思いつくのにかなりの時間が掛かっています.

これで単一のファイルで画像にさざなみフィルターを掛けることが可能となりました.このようにフィルターのクロスブラウザ化は非常に面倒であり,実装には多くのテクニックが必要となる場合がある点を憶えておきましょう.


<?xml version="1.0" standalone="no"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" 
 xmlns="http://www.w3.org/2000/svg" 
 xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
 <defs>
  <!--さざなみグラデーション-->
  <radialGradient id="rg" cx="0.5" cy="0.5" r="0.25" spreadMethod="repeat">
   <stop offset="0" stop-color="red">
   <animate id="anim" attributeName="stop-opacity" 
   calcMode="linear" begin="0s" dur="6s"
   values="0;1;0" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   </stop>
   <stop stop-color="red">
   <animate attributeName="offset" 
   calcMode="linear" begin="0s" dur="6s"
   values="0;0;0.5" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   <animate attributeName="stop-opacity" 
   calcMode="linear" begin="0s" dur="6s"
   values="0;1;1" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   </stop>
   <stop  stop-color="red" stop-opacity="0">
   <animate attributeName="offset" 
   calcMode="linear" begin="0s" dur="6s"
   values="0;1" keiTimes="0;1" repeatCount="indefinite"/>
   </stop>
   <stop stop-color="red">
   <animate attributeName="offset" 
   calcMode="linear" begin="0s" dur="6s"
   values="0.5;1;1" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   <animate attributeName="stop-opacity" 
   calcMode="linear" begin="0s" dur="6s"
   values="1;1;0" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   </stop>
   <stop offset="1" stop-color="red">
   <animate attributeName="stop-opacity" 
   calcMode="linear" begin="0s" dur="6s"
   values="0;1;0" keiTimes="0;0.5;1" repeatCount="indefinite"/>
   </stop>
  </radialGradient>
  <!--周辺を暗くするグラデーション-->
  <radialGradient id="rg2" cx="0.5" cy="0.5" r="1">
   <stop offset="0%" stop-opacity="0"/>
   <stop offset="100%" stop-opacity="1"/>
  </radialGradient>
  <!--さざなみフィルター(chrome/opera用)-->
  <filter id="sazanami" filterUnits="userSpaceOnUse" x="0" y="0" width="200" height="200">
   <feImage xlink:href="#wave" result="wave"/>
   <feGaussianBlur in="wave" stdDeviation="8" result="wave2"/>
   <feDiffuseLighting in="wave2" lighting-color="white" 
    surfaceScale="50" diffuseConstant="0.3" result="wave3">
    <feDistantLight azimuth="-90" elevation="60"/>
   </feDiffuseLighting>
   <feDisplacementMap in="SourceGraphic" in2="wave2" scale="-10" result="wave4"/>
   <feBlend in="wave3" in2="wave4" mode="screen"/>
  </filter>
  <!--さざなみフィルター(firefox用)-->
  <filter id="sazanamiF" filterUnits="userSpaceOnUse" x="0" y="0" width="400" height="200">
   <feOffset in="SourceGraphic" result="wave" dx="-200" 
    x="0" y="0" width="200" height="200"/>
   <feGaussianBlur in="wave" stdDeviation="8" result="wave2" 
    x="0" y="0" width="200" height="200"/>
   <feDiffuseLighting in="wave2" lighting-color="white" 
    surfaceScale="50" diffuseConstant="0.3" result="wave3" 
    x="0" y="0" width="200" height="200">
    <feDistantLight azimuth="-90" elevation="60"/>
   </feDiffuseLighting>
   <feDisplacementMap in="SourceGraphic" in2="wave2" scale="-10" result="wave4" 
    x="0" y="0" width="200" height="200"/>
   <feBlend in="wave3" in2="wave4" mode="screen" 
    x="0" y="0" width="200" height="200"/>
  </filter>
  <g id="wave">
   <rect width="200" height="200" fill="url(#rg)"/>
   <rect width="200" height="200" fill="url(#rg2)"/>
  </g>
 </defs>
 <!--ブラウザの種類で分岐-->
 <switch>
  <!--SVG-animation=true…chrome/opera-->
  <g filter="url(#sazanami)" requiredFeatures="http://www.w3.org/TR/SVG11/feature#SVG-animation">
   <rect x="0" y ="0" width="200" height="200" fill="blue"/>
   <image xlink:href="http://jsrun.it/assets/p/S/G/q/pSGqB.png" width="200" height="200"/>
  </g>
  <!--SVG-animation=false…firefox-->
  <g filter="url(#sazanamiF)">
   <rect x="0" y ="0" width="200" height="200" fill="blue"/>
   <image xlink:href="http://jsrun.it/assets/p/S/G/q/pSGqB.png" width="200" height="200"/>
   <use x="200" y="0" xlink:href="#wave"/>
  </g>
 </switch>
</svg>

0 件のコメント:

コメントを投稿