次の方法で共有


scriptjunkie{}

HTML5 と SVG でゾンビの黙示録を生き延びる (パート 3): JavaScript で SVG を操作する

Justin Whitney | 2013 年 6 月 26 日

以前の資料のダウンロード

多くの方が黙示録を生き延びそうです。あるいは、少なくとも HTML5 アプリをビルドできそうです。難しくなるか、簡単になるのかは、アプリと黙示録しだいです。これまで パート 1 (英語) では、テキスト、画像、円、矩形、そして (最も重要) なパスなどの SVG の静的要素を取り上げました。 パート 2 (英語) では、JavaScript 使わなくても実行できる魅力的なアニメーション技法をいくつか使用してみました。

ただし、これまでのアプリも、現状のままではゾンビからだれかを救い出す助けにはなりません。そのためには、操作するボタンなど、いくつか要素を加える必要があります。シリーズのパート 3 となる今回のチュートリアルでは、ユーザーの操作に応答するために 2 つの異なるメソッドを導入し、SVG 要素自体の内部に属性アニメーションを盛り込み、JavaScript を使用して Core DOM 要素と SVG DOM 要素を操作します。

注: 今回のコードは、シリーズ パート 2 の最終版のソース コードを土台にビルドします。パート 2 のコードは、 http://justinwhitney.com/zombies/zombies_part2.htm のソースを表示して確認できます。また、今回のチュートリアルでは新しい画像もいくつか追加します。シリーズの 以前の資料 (英語) も参考にしてください。

<set> 要素の使用

前回のチュートリアルでは、<animateMotion> という小さな要素を <image> 要素に埋め込み、<image> 要素にモーションのパスと継続期間を定義することにより、ゾンビが動き始めました。これは SVG アニメーションのほんの触りにすぎません。アニメーションは、コントロールの視覚的なインジケーターをユーザーに提供する (ページに魅力的な要素を加える) 以外に、ユーザー操作に反応していることを示すためにも使用できます。

以前は、JavaScript を使用して画像を入れ替えたり、CSS を使用してポイントされたときに表示するスタイルを定義するなど、数多くの類似テクニックを駆使してこのようなアニメーションを実装してきました。SVG は、要素自体の中にプロパティの変化を埋め込み、その変化をマウス イベントに結び付けることができるようにして、このようなテクニックに SVG 独自のツールを追加します。最もよく使われる例の 1 つは、<set> 要素を使用してストロークや塗りつぶしの色を変更する方法です。

揺れ動くゾンビに戻ります。現時点では、最初に定義されたように、ゆっくり動くゾンビを赤色の太いストロークの円で囲み、動きの速いゾンビは黒色の細いストロークの円で囲んでいます。ユーザーが動きの速いゾンビをクリックすると、2 つが入れ替わると面白くなるのは明らかです。普通は <image> 要素をクリックに反応させるには何かを追加しなければなりません。しかし、この場合はその必要がありません。

ID "fastCircle" の <circle> 要素を見てみましょう。現時点では、この要素を次のように定義しています。

<circle id="fastCircle" cx="275" cy="325" r="40" stroke="black" fill="white" stroke-width="2" />

この要素をクリックに反応させるには、ストロークとストロークの幅に起こり得るすべての変化とその変化を引き起こすマウス イベント (今回重要な部分) を定義する <set> 要素を組み込みます。今回の場合、"fastZombie" 画像が mousedown イベントを受け取ると "fastCircle" の属性を変化させます。構文は次のようになります。

<circle id="fastCircle" cx="275" cy="325" r="40" stroke="black" fill="white" stroke-width="2">
  <set attributeName="stroke" from="black" to="red" begin="fastZombie.mousedown"  />
  <set attributeName="stroke-width" from="2" to="4" begin="fastZombie.mousedown"  />
</circle>

(要素の終了を </circle> 終了タグに変更していることに注意してください)。もちろん、"fastCircle" 要素は "slowZombie" 画像のクリックにも反応する必要があるので、その処理も行います。

<circle id="fastCircle" cx="275" cy="325" r="40" stroke="black" fill="white" stroke-width="2">
  <set attributeName="stroke" from="black" to="red" begin="fastZombie.mousedown"  />
  <set attributeName="stroke-width" from="2" to="4" begin="fastZombie.mousedown"  />
  <set attributeName="stroke" from="red" to="black" begin="slowZombie.mousedown"  />
  <set attributeName="stroke-width" from="4" to="2" begin="slowZombie.mousedown"  />
</circle>

また、"slowCircle" 要素にも逆方向に同様の処理が必要です。

<circle id="slowCircle" cx="75" cy="325" r="40" stroke="red" fill="white" stroke-width="4">
  <set attributeName="stroke" from="black" to="red" begin="slowZombie.mousedown"  />
  <set attributeName="stroke-width" from="2" to="4" begin="slowZombie.mousedown"  />
  <set attributeName="stroke" from="red" to="black" begin="fastZombie.mousedown"  />
  <set attributeName="stroke-width" from="4" to="2" begin="fastZombie.mousedown"  />
</circle>

これだけの追加で、JavaScript を使用していないにもかかわらず、円がユーザー操作に反応するようになります (図 1 参照)。


図1. <set> を使用した mousedown に基づくストローク属性の変更

JavaScript による <text> DOM の操作: textContent

<set> を使用するのは洗練された技法ですが、1 つ大きな問題があります。SVG 対応のブラウザーすべてがこの機能を実装しているわけではありません。さいわい、XML ベースの仕様である SVG には、JavaScript から Core DOM 仕様に基づくすべてのドキュメントにアクセスするのと同じ方法 (getElement() と setElement() を使用) でアクセスできます。そこで、ブラウザーとの互換性を最大限に高めるために、setSpeed(speed) という新しい関数を追加します。

<script>
 
function setSpeed(speed) {
  if (speed == 'Fast') {
    var circleSelected = document.getElementById('fastCircle');
    var circleUnselected = document.getElementById('slowCircle');
  } else {
    var circleSelected = document.getElementById('slowCircle');
    var circleUnselected = document.getElementById('fastCircle');
  }
  circleSelected.setAttribute('stroke','red');
  circleSelected.setAttribute('stroke-width','4');
  circleUnselected.setAttribute('stroke','black');
  circleUnselected.setAttribute('stroke-width','2');
}
 
</script>

この関数は、fastCircle 要素と slowCircle 要素を取得し、これらの要素に直接アクセスして stroke 属性と stroke-width 属性を設定します。

これでユーザーがクリックすると、動きの速いゾンビとゆっくり動くゾンビを囲む円が入れ替わるようになるので、同様にスピードを示すテキストも変更します。テキストの入れ替えも同じテクニック (Core DOM 経由で SVG の属性にアクセス) を使用して行います。ただし、SVG DOM 経由で要素の属性に直接アクセスできる場合もあります。このようにアクセスできれば、コードが簡潔になるだけでなく、パフォーマンスも向上します。ポイントは、その属性に必要な構文を把握することです。

<text> 要素のコンテンツ (textContent) は、SVG DOM を使って直接アクセスできる属性の 1 つで、以下のように使用します。

function setSpeed(speed) {
  if (speed == 'Fast') {
    var circleSelected = document.getElementById('fastCircle');
    var circleUnselected = document.getElementById('slowCircle');
  } else {
    var circleSelected = document.getElementById('slowCircle');
    var circleUnselected = document.getElementById('fastCircle');
  }
  circleSelected.setAttribute('stroke','red');
  circleSelected.setAttribute('stroke-width','4');
  circleUnselected.setAttribute('stroke','black');
  circleUnselected.setAttribute('stroke-width','2');
  var speedText = document.getElementById('speedText');
  speedText.textContent = speed;
}

テキストを入れ替えるには、それぞれのゾンビの画像に onmouseup イベントを追加します。

<image id="slowZombie" x="375" y="1875" width="175" height="304" transform="scale(.16,.16)"
  xlink:href="zombie.svg" onmouseup="setSpeed('Slow');">

<image id="fastZombie" x="1630" y="1875" width="175" height="304" transform="scale(.16,.16)"
  xlink:href="zombie.svg" onmouseup="setSpeed('Fast');">

ゾンビ画像をクリックすると、画像を囲む円だけでなく、テキストも変化するようになります (図 2 参照)。


図2. <text> 要素の textContent の変化

SVG DOM と Core DOM の比較の詳細については、MSDN の IEBlog (英語) を参照してください。IEBlog では他のベスト プラクティスも取り上げています。SVG DOM 仕様の詳細については、 こちら (英語) をご覧ください。

新しい SVG 要素の追加

パート 1 では <path> 要素を導入し、この要素を複数使用して controlPanelBox ペイン内にインクリメント コントロールとデクリメント コントロールを作成しました。今回は、JavaScript の力を借りて、これらのコントロールに命を吹き込みます。まず新しいゾンビを作成し、次にレッドネックとビルディングを追加して、最後にこれらの要素をデクリメント コントロールで削除します。

名前空間の定義を基に新しい要素を作成するのに慣れていれば、document.createElementNS コマンドには見覚えがあるでしょう。このコマンドも新しい SVG 要素を作成する鍵になります。

ヘッダーで newZombie() という新しい JavaScript 関数を作成します。徐々にコードを堅牢にしていきますが、この時点では次のように "http://www.w3.org/2000/svg" 名前空間の "image" 定義を参照することによってゾンビを作成します。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
}

SVG 要素を作成したら、その直後でその要素の属性にちょっと変わった操作を行います。<image> 要素の大半の属性は setAttribute を使って参照できますが、画像自体のソース (xlink:href 属性) は参照できません。この属性は、ソースの仕様を参照して定義する必要があります。今回の場合は "http://www.w3.org/1999/xlink" 名前空間の href 定義です。

W3 wiki (英語) ではこの混乱を取り上げ、新しい <image> 要素の作成する際に最もよくある間違いと指摘しています。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
  svg.setAttributeNS('http://www.w3.org/1999/xlink','href','zombie.svg');
}

このシリーズの前半で、ゾンビの <image> 要素に速度のコントロールを配置する際に、画像を完全なクロスブラウザー互換にするのに非常に複雑な処理を必要としました。直感的には、画像に必要な幅と高さを設定し、目的の座標に配置すれば、希望する結果が得られると考えます。ほとんどのブラウザーでこの直感があたっています。しかし、例外もあり、いくぶん拡大縮小が必要なブラウザーもあります。たとえば、slowZombie の <image> 要素の定義をもう一度見てください。

<image id="slowZombie" x="375" y="1875" width="175" height="304" 
  transform="scale(.16,.16)" xlink:href="zombie.svg" onclick="setSpeed('Slow');">

目標は、50 x 50 の画像を配置することでした (厳密に言えば高さが 50 で幅は高さに比例させます)。実際の zombie.svg ソースでは 175 x 304 の画像を定義しています。そのため、目標の大きさにするため、<image> 要素のサイズを 175 x 304 と定義して、transform:scale を使って .16 という倍率を設定しています。この拡大縮小により、x 座標と y 座標にも倍率をかけて 60 と 300 に変更します。

新しい <image> 要素を動的に作成するときも、同様の処理が必要です。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
  svg.setAttributeNS('http://www.w3.org/1999/xlink','href','zombie.svg');
  svg.setAttribute('width','175');
  svg.setAttribute('height','304');
}

ただし、先ほどの倍率に基づいて x 座標と y 座標を計算し、求めた座標に画像を配置するのではなく、別の方法を試してみます。ここでは transform:translate を使用してゾンビの位置を設定します。transform:translate は要素の原点を再定義します。したがって、たとえば、原点が (0, 0) のキャンバスの座標 (50,100) にオブジェクトを配置するのではなく、原点が (50,100) のキャンバスの座標 (0, 0) にオブジェクトを配置します。

svg.setAttribute('transform','translate(50, 100)');

複数の変換を同一行にまとめられるよう、"scale" transform を使って関数を完成します。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
  svg.setAttributeNS('http://www.w3.org/1999/xlink','href','zombie.svg');
  svg.setAttribute('width','175');
  svg.setAttribute('height','304');
  var scale = .16;
  var x = Math.floor(Math.random()*550);
  var y = Math.floor(Math.random()*350);
  svg.setAttribute('transform','translate(' + (x) + 
    ', ' + (y) + ') scale(' + scale + ', ' +
    scale + ')');
  document.getElementById('cityBox').appendChild(svg);
}

今回の例では、(サイズが 50 x 50 の画像自体が収まるように) 600 x 400 の "cityBox" ペインのランダムな位置に (x, y) 座標をセットします。原点 (0, 0) は、既定では左上隅です。最後に、新しい要素を他の要素と同様 DOM に追加し、今回は "cityBox" <svg> 要素をその親要素に指定します。

関数を呼び出すには、ID "zombieMore" の <path> 要素の onmouseup イベントに newZombie() 関数を追加します。これは "City Population (000s)" のインクリメント ボタンの要素で、黙示録の中でゾンビの発生率を予測する際に重要な因子になります。今のところ、これはテストにすぎないため、他のボタンについては気にしません。

<path id="zombieMore" d="M 300 50 l -50 -25 l 0 50 l 50 -25" stroke="black" stroke-width="1"
  fill="red" onmouseup="newZombie();" />

当然、出力位置はランダムに変わりますが、新しく有効になったインクリメント ボタンをクリックすると、新しいゾンビが都市のあちこちに出現します (図 3 参照)。


図3. 実行

DOM 操作は機能しますが、見栄えの点でいくつかコードに微調整が必要です。まず、食欲旺盛なゾンビというのは間違いなく魅力的で、おそらく人間の脳を捕食することになりますが、これはゲームの最終局面のシナリオです。"生き残り" 予測機能を実現するには、逃げ回る人間の周りに安全地帯を設置することに取り組むことにします。次に、人間の能や叫び声に近づかない限り、ゾンビは (歩きながらメールを打とうとする人のように) 方向感覚に難があります。そこで、変化に富んだ状況を演出するために、一部の画像は水平方向に反転するようにします。

最初の調整は基本的な JavaScript を使って実現します。次のコードを使用して、絶叫する人間の周りに 200 x 100 の安全地帯を設けます。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
  svg.setAttributeNS('http://www.w3.org/1999/xlink','href','zombie.svg');
  svg.setAttribute('width','175');
  svg.setAttribute('height','304');
  var scale = .16;
  var x = Math.floor(Math.random()*550);
  var y = Math.floor(Math.random()*350);   
  var cityWidth = 600;
  var cityHeight = 400;
  var safezoneWidth = 200;
  var safezoneHeight = 100;
  var safezoneX = Math.round((cityWidth - safezoneWidth) / 2, 0);
  var safezoneY = Math.round((cityHeight - safezoneHeight) / 2, 0);
 
  if ( ((safezoneX - 50) <= x) && (x <= (safezoneX + safezoneWidth)) &&
  ((safezoneY - 50) <= y) && (y <= (safezoneY + safezoneHeight)) ) {
    switch (Math.floor(Math.random()*4)) {
      case 0:
      x = safezoneX - 50;
      break;
      case 1:
      x = safezoneX + safezoneWidth;
      break;
      case 2:
      y = safezoneY - 50;
      break;
      case 3:
      y = safezoneY + safezoneHeight;
      break;
    }
  }
 
  svg.setAttribute('transform','translate(' + (x) + 
    ', ' + (y) + ') scale(' + scale + ', ' +
    scale + ')');
  document.getElementById('cityBox').appendChild(svg);
}

SVG には直接関係ありませんが、上記のコードは画像の配置に影響する処理をいくつか行います。まず、絶叫する人間が 600 x 400 のキャンバスの中心に位置するものとして、200 x 100 の安全地帯の原点の x 座標と y 座標を計算して設置します。次に、ゾンビの現在の x,y 座標が安全地帯の中に入っている場合は、移動方向をランダムに選んで安全地帯の外に押し出します。

図 4 は、波のように押し寄せるゾンビを表示して、安全地帯がどのようになるかを示しています。


図4. 安全地帯

少しよくなりましたが、まだ、ゾンビが蔓延しているというよりは、ブラック フライデー セールのように見えます (微妙な違いですが、違いは違いです)。ゾンビの一部が逆を向いていたら、少しはましに見えるでしょう。ところが、問題発生です。このシリーズのパート 2 で示したように、transform 属性はまったく新しいマトリックスを拡大縮小したり、回転したり、ゆがめたり、平行移動したり、定義したりはできますが、水平方向にも垂直方向にも反転することはできません。痛い見過ごしですが、translate と scale を組み合わせればこの問題を打開できます。

つまり、倍率を負の数にすることで、要素を垂直方向または水平方向に反転できます。ただし、この方法は、キャンバスの原点と相対に要素を拡大縮小します。したがって、原点を (0, 0) のままにして倍率を(-1, 1) にすると、要素は反転しモニターの左側に向かって進み、表示されないエリアに移動します。画像は存在し、変換も行われますが、事実上要素は見えなくなります。

さいわい、どのブラウザーでも対応できる画像配置技法、具体的には transform:translate 属性と transform:scale 属性する技法により、負の倍率を簡単に付加できます。

まとめると、次のようになります。

  • transform:flip という属性はない (存在しない)。
  • transform:scale(-1, 1) を単独で使用すると、画像全体が <svg> 親要素外部に反転される。
  • transform:translate と transform:scale の組み合わせを効果的に使えば画像を適切に反転できる (この場合 transform:scale は 2 つの役割を果たし、希望するサイズに画像の拡大縮小も行なう)。

つまり、次のように適切なランダム化、画像サイズを相殺するための配置調整、および transform:scale コードへの微調整を加えます。

function newZombie() {
  var svg = document.createElementNS("http://www.w3.org/2000/svg","image");
  svg.setAttributeNS('http://www.w3.org/1999/xlink','href','zombie.svg');
  svg.setAttribute('width','175');
  svg.setAttribute('height','304');
  var scale = .16;
  var x = Math.floor(Math.random()*550);
  var y = Math.floor(Math.random()*350);   
  var cityWidth = 600;
  var cityHeight = 400;
  var safezoneWidth = 200;
  var safezoneHeight = 100;
  var safezoneX = Math.round((cityWidth - safezoneWidth) / 2, 0);
  var safezoneY = Math.round((cityHeight - safezoneHeight) / 2, 0);
  if ( ((safezoneX - 50) <= x) && (x <= (safezoneX + safezoneWidth)) &&
  ((safezoneY - 50) <= y) && (y <= (safezoneY + safezoneHeight)) ) {
    switch (Math.floor(Math.random()*4)) {
    case 0:
    x = safezoneX - 50;
    break;
    case 1:
    x = safezoneX + safezoneWidth;
    break;
    case 2:
    y = safezoneY - 50;
    break;
    case 3:
    y = safezoneY + safezoneHeight;
    break;
    }
  }
  flip = Math.floor(Math.random()*2)*2-1;    //results in -1 or 1
  x += 25 - 25*flip;    //adjust for 50x50 zombie size; results in +50 or +0
  svg.setAttribute('transform','translate(' + (x) + 
    ', ' + (y) + ') scale(' + (scale * flip) + ',
    ' + scale + ')');
  document.getElementById('cityBox').appendChild(svg);
}

その結果、黙示録はさらに混沌とした状態に変わります (図 5 参照)。


図5. ゾンビ、反転ゾンビ、transform:translate と transform:scale の適用

現時点での「ゾンビの黙示録生き残り予測」の動作については、 http://justinwhitney.com/zombies/zombies_part3.htm を参照してください。サンプル コードについては、そのページのソース コードをご覧ください。

まとめ

今回は、見た目を本格的にする作業を始めました。哀れで出来が悪い人間に絶望的な状況が迫りつつあります。今、この絶叫する人間を助けられる可能性があるのはショッピング モールとレッドネックの 2 つだけです。ただし優れた連載シリーズは次回新たな展開を迎えます。レッドネックはこの人間を安全なところまで送り届けられるでしょうか、時間内にショッピング モールを見つけ、貪欲なゾンビの大群から逃げ切れるでしょうか。お楽しみに。


この記事は、Internet Explorer チームによる HTML5 技術シリーズの一部です。3 か月間無料の BrowserStack クロスブラウザー テスト ( https://modern.IE) を使って、この記事の概念をお試しください。


執筆者紹介

Justin Whitney は、アメリカ南部在住のプログラマ、ライター兼映画プロデューサーです。彼が住む南部地域では、レッドネックやショッピング モールのそばにいることで少し自分の身が守られます。彼の連絡先は、 webmaster@justinwhitney.com (英語のみ) です。