星の描き方

◆ はじめに ◆

Scalable Vector Graphics,略してSVGという画像フォーマットが, にわかに脚光を浴びています.XMLに基づくデータフォーマット(テキストファイル) であること,スケーラブルすなわち拡大縮小してもギザが生じないなどの メリットのほか,Adobe SVG Viewer,Batik SVG Toolkitといった 表示ソフトウェアが無料で入手できることもまた,手軽さの一因となっています.

その一方で,ファイル作成のほうはというと,情報が点在しているように 感じます.雑誌の特集記事をちょくちょく見かけますが,たいていは, コードと表示例のペアでもって「SVGというものの紹介」で終わっています (著者さんはエネルギーを投入して解説記事を書いているにも関わらず). SVGのリファレンスもいくつか存在します.しかし,「SVGを操る方法」… もちろん見る方法ではなく創る方法…は少ないように思えます.

この文書は,SVGを操る方法を修得するために筆者が苦労したことがらを とりまとめ,これを読んでもらうことで,読者にも,SVGで絵を創る いくつかの方法を知ってもらう,そんなことを意図して執筆しました.

SVGの仕様は, Scalable Vector Graphics (SVG) 1.0 Specification に集約されています.日本語版は, Scalable Vector Graphics (SVG) 1.0 仕様書 です. この文書では,これらの仕様書なしで読めるように工夫していますが, 少しいじってユーモラスな絵を描きたいというときには,これらの サイトが大いに役立つことでしょう.

「SVGで絵を描くこと」とは, 「テキストエディタで1文字1文字入力していくこと」ではなく, 「新しいバージョンのIllustratorなどでパーツを置いていくこと」でもありません. 筆者は基本的にテキストエディタを使用していますが, 複雑な計算を要するところでは,別のデータフォーマットやプログラミング言語で 記述し,変換プログラムを実行して,その出力をSVGに埋め込むということを 積極的に取り入れています.具体的には,Encapluted PostScript (EPS)や, 国産のオブジェクト指向スクリプト言語として知られる Ruby を愛用しています. これらの細かい説明に分量を費さないようにしていますが, 少しいじってみたいときのために,解説をつけていることもあります.

この文書では,Linuxユーザを対象としているところがあります. 実際,筆者はturbolinux7 Workstation(製品版),Vine Linux 2.5(FTP版), Kondara MNU/Linux 2.0(製品版)で,SVG画像のチェックをしています. シェルにはbashを用いています.それ以外の環境でも容易に再現できると 思いますが,コマンドの実行などでは,環境に応じて読み替えをしてください.

また,XMLの書式,ここではとりあえず「well-formedなXML文書」の書き方 (XMLのいわゆる文法)を知っているものとして話を進めます.

◆ 準備 ◆

まずは,SVG画像を見る環境を整えておきましょう.(そして,残念ながら, 現在の主要なLinuxディストリビューションにおいて,SVG閲覧ツールは パッケージに含まれていないようです.) ここでは,筆者が好んで利用している2種類のソフトウェア, Adobe SVG ViewerとBatik SVG Toolkitを紹介します.

◆ ◆ Adobe SVG Viewer ◆ ◆

Adobe SVG Viewer,以下ではSVG Viewerと略しますが,これは, Webブラウザ上でSVG画像を表示するためのプラグインです. もちろん,コンピュータの中に入っている(専門用語で言うと,ローカルの) ファイルを開くなり,Windowsなら,Webブラウザのショートカットもしくは 起動中ウィンドウにドラッグ&ドロップするなりすれば,見ることが可能です.

SVG ViewerのWebサイト(日本語)は http://www.adobe.co.jp/svg/ です. Linux用のSVG Viewerも,ベータ版として存在します. そのインストール方法は,拙稿ですが http://zmp.s1.xrea.com/mozilla+svg.html にまとめられています.

Webブラウザを必要とするものの,表示までの時間は比較的短く, アニメーションにも対応しているので,SVG画像を見るには現段階で 最適の方法だと思います. HTML中に埋め込んだSVG画像(インライン画像)を見ることもできます. 欠点は,拡大縮小の機能が(あるにはあるのですが)うまく働いていないこと でしょうか.まあ,SVGファイルで拡大縮小が割と簡単なので, 致命的な欠陥というわけではありません.

◆ ◆ Batik SVG Toolkit ◆ ◆

Batik SVG Toolkit,以下ではBatikと略しますが,これは, Javaアプリケーションとして,SVG画像の表示や変換をすることのできる ソフトウェア群です.Tomcatと組めば,効率よく動的にSVGファイルを生成する Webアプリケーションが構築できると思われます.この文書では, SVG画像の表示・変換ツールとして,使い方を簡単に紹介します.

BatikのWebサイトは, http://xml.apache.org/batik/ です.日本語訳も存在し, http://www.kit.hi-ho.ne.jp/%7eginga/batik/ で読むことができます.

Java実行環境が必要です.Batikはバイナリ配布のため,コンパイルは不要です. JREがあれば十分でしょう.とはいうものの筆者は, Java 2 Platform, Standard Edition (J2SE) バージョン1.4 をインストールしています.

Batikは,SVG画像を見ることのできる「SVG Browser」や, SVG画像を,PNGやJPEGといったラスタ型画像フォーマットに変換する 「SVG Rasterizer」といったソフトウェア(jarファイル)を含みます. Java実行環境とBatikをインストールしたら,シェルのエイリアス機能を 使って,コマンド名を用意しておきましょう. そのためにはまず,batik-svgbrowser.jarとbatik-rasterizer.jarのファイルが どのディレクトリにあるかを調べます. 筆者の環境ではともに/usr/local/javadir/batik-1.1.1にありますので,
alias svgb='java -jar /usr/local/javadir/batik-1.1.1/batik-svgbrowser.jar'
alias svgr='java -jar /usr/local/javadir/batik-1.1.1/batik-rasterizer.jar'
と指定しています(~/.bashrcなどに追加しておき,シェルを立ち上げ直します). こうすることで,
[プロンプト] svgb star.svg &
を実行すれば,star.svg を表示してくれます. その調子で,
[プロンプト] svgr star.svg
を実行すると,star.pngを生成してくれます. ただしそのままだと,範囲指定の都合で,左上にずれた絵が生成されることがあります(これでうまくいく場合もあります.なんでや?). かわりに
[プロンプト] svgr -a -200,-200,400,400 star.svg
と実行すると,期待通りの絵になります. また,
[プロンプト] svgr -a -200,-200,400,400 -m image/jpeg star.svg
とすると,JPEG画像のファイルstar.jpgが生成されます(が,手元の環境ではうまく生成してくれません).

Batikの残念なところは, SVGのアニメーションに対応していないのと, 単一の変換だとJava実行環境の立ち上げに時間がかかることです. しかし, WebブラウザなしでSVG画像を見るのや, たくさんのSVG画像を一括してPNG画像に変換するのには 極めて便利なソフトウェアです.

◆ 星の描き方 ◆

ここでは,「星型」を描くSVG画像の制作過程を紹介します. 星型はなじみのある図形ですが,座標をとって点を結んで…と考えると 少し頭の痛い作業です.つまり,SVGの仕様を知っているだけでは, 画像づくりは簡単にいかないということです. それを解決するために,座標を求めるプログラムを作成しました. このプログラムを少しいじれば,いろんな星型を描くことができます.

◆ ◆ 目指すもの ◆ ◆

星型,とは,こんな図形です.

ファイル名: star00.svg
数学的に言うと,正五角形の対角線です.

ですがこの図形は,領域内の塗りつぶしに注意が必要で, そのほか拡張性においてやや面白みが欠けるため, こちらの図形を描くことを目標にしてみます.

ファイル名: star01.svg

これは十角形です.辺の長さはみな同じですが,角度はみな同じという わけにはいかないので,正十角形ではありません.

この図形を描く場合,10個の点の座標が必要になります. 実際,SVG画像でもそのように指定しています. しかしこれらを手で求めるのは厄介ですし,計算ミスも起こり得ます. そういった厄介なところは,プログラムで計算させるべきでしょう.

◆ ◆ 極座標で見る ◆ ◆

この10個の点の座標…直交座標系(XY座標系)の値…を求めるために, 何かいい方法がないか,図を見て考えてみます.

星型をじっとにらんでみると,この図形の中心から, 5つの点が同じ距離に,残る5つの点も同じ距離にあることが わかります.わかるというより,対称性から明らかですね.

補助線(直線と曲線)を引いてみましょう.

ファイル名: star02.svg
このとき, ということがわかります.

赤の円の半径を100,青の円の半径をとりあえずaとしておきます. (0<a<100です.SVGファイルを見ればaの値がいくらなのかすぐわかると思いますが, そういうことはしないでくださいね.) それから,上で書いたように,角度に関する情報もわかっています. 星型の各点が,原点を中心として配置できるとなると…ここは, 極座標の出番でしょう.

極座標系では,点を,原点からの距離r,それと偏角θというふたつの値で 表現します.偏角の概念は高校なり大学なりで学んでいればいいのですが, これも図にするのがいいのでしょうね.

ファイル名: polar.svg

点を(r,θ)で表記するなら,そして第2成分の単位を「度」(360度で1周)とするなら, この星型の10個の点は,最も上にある点から時計回りに, (100,90),(a,126),(100,162),(a,198),(100,234),(a,270),(100,306),(a,342),(100,18),(a,54) となります.一般化すると,それぞれの点の座標は (ri,θi) です.ここで, iは0〜9の整数, ri は,iが偶数のときは100で奇数のときはa, θi は,90+36*iを360で割った余りです. (あとの計算では,360で割って余りを求める必要はありませんが.) なぜ一般化しておくのかというと,それは,効率のいいプログラムを作るためです.

◆ ◆ 極座標から直交座標へ ◆ ◆

SVGの座標系は直交座標系です. そこで,極座標で示した上記の各点を, XY座標系に変換する方法を検討しておきましょう. といっても,極座標の点(r,θ)を,直交座標系の点(x,y)に変換する式は, これだけです.高校の数学の教科書に書かれていると思います.

x=r*cos(θ)
y=r*sin(θ)

これであとは計算するだけ…と言えればいいのですが, SVGには少し注意しないといけない問題があります.それは, SVGにおける座標系(「コンピュータ座標系」と呼ぶことができますが, 以下では,SVG座標系と呼びましょう)は, 数学で常識となっているXY座標系と比べて,上下が逆という違いがあるのです. 数学では,yの値を大きくすると上に進みますが, SVGでは,yの値を大きくすると下になります. SVG座標系を,図にします.

ファイル名: svgaxes.svg
他の多くの画像フォーマットあるいはディスプレイ時の座標系と異なり, SVGの座標系ではxやyの値が負であってもかまいません.

先ほど求めた,星型の極座標による点(r,θ)を,SVG座標系の点(x,y)に変換するには, こうします.

x=r*cos(θ)
y=-r*sin(θ)
yの符号を反転するだけです.

◆ ◆ 変換プログラム ◆ ◆

ではその,10個の点のSVG座標を求めるプログラムを作りましょう. 極座標ではaという文字を使っていましたが,ここでは53という値にしてみます. 初めにお見せした星型よりもふっくらした星を描くことができます.

ファイル名: calc00.rb
#!/usr/bin/env ruby

h1 = 100.0
h2 = 53.0

def my_sin(angle)
  Math::sin(angle.to_f * Math::PI / 180.0)
end

def my_cos(angle)
  Math::cos(angle.to_f * Math::PI / 180.0)
end

0.upto(9) do |i|
  if i % 2 == 0 then r = h1 else r = h2 end
  theta = 90 + i * 36
  printf "%-7.3f %-7.3f\n", r * my_cos(theta), -r * my_sin(theta)
end

この文書で初めて登場するRubyスクリプトなので,このファイルの解説は 少し詳しく行っておきましょう.最初の行は,このファイルをrubyという プログラムに解釈させることを指示します. ああ,そうでした,実行する前に,このrubyというコマンドが使えるように しておかないといけません. 最近のメジャーなLinuxディストリビューションには組み込まれていることが多く, もしないとしても,ソースをダウンロードしてビルドするだけです. 「#!/usr/bin/env ruby」は,Rubyスクリプトを実行する際のおまじないです. 実はこの行がなくても問題にならないよう,この文書では配慮していますが, 一目でRubyスクリプトとわかるよう,つけておきます.

h1とh2には,中心からの距離を表す定数を代入しておきます. RubyはPerlと異なり,文の最後に「;」はいりません. (ひとつの行で複数の文などを書くときは必要になります.) 次に,my_sin,my_cosというふたつの関数を定義しています.それぞれ, 角度を入力にとり,そのsinもしくはcosを返します.Rubyでは,というか 多くのプログラミング言語では,sinやcosなど三角関数の引数には 弧度法(360度を2πとする単位)を採用しているので,そのための変換も, my_sinやmy_cosの中で行っています. PerlもそうですがRubyでは,途中で抜ける場合を除き, 関数内で最後に実行する文の値が,その関数の返り値になります.

関数内で2度出現する「.to_f」は,直前の値(このプログラムではangle)を 浮動小数点の値に変換します. このプログラムの場合には除去しても結果に変わりがありませんが, 整数と浮動小数点数が混合する計算をする場合, つけておくのが無難だと思います.

「0.upto(9) do |i| … end」はRuby独特の表現です. Cで書くと,「for (i = 0; i <= 9; i++) { … }」とほぼ等価です. つまり,iの値を0から9までひとつづつ増やしてループ内を実行せよ, ということです. 中では,10個の点ごとに,まず極座標を(r,theta)に代入してから, 次にSVG座標へ変換すると同時に出力しています. 奇偶判定で「i % 2 == 0」と書いていますが, CやPerlと違って「== 0」を省略できません. というのは,Rubyでは0も,真か偽かというと真になるからです. Rubyにおいて偽となるのは,評価の結果falseもしくはnilになる値のみです.

これでプログラムの解説を終わりますが,最後にひとつ. Rubyのファイルは,「.rb」で終わらせるのが慣例です. このファイルは,calc00.rbという名前にしておきます.

実行結果はこの通り.
[プロンプト] ruby calc00.rb
0.000   -100.000
-31.153 -42.878
-95.106 -30.902
-50.406 16.378 
-58.779 80.902 
-0.000  53.000 
58.779  80.902 
50.406  16.378 
95.106  -30.902
31.153  -42.878

◆ ◆ SVGにする ◆ ◆

ここでやっと,SVGファイルを作る方法を書くことができます. 天下りの感もありますが,出来上がりはこうなります.

ファイル名: star03.svg
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" 
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg width="400" height="400" viewBox="-200 -200 400 400">
 <g transform="translate(0,16) scale(2)" style="fill:none;stroke:black;stroke-width:5;stroke-linejoin:round;">
  <path
   d="M0.000   -100.000
L-31.153 -42.878
-95.106 -30.902
-50.406 16.378 
-58.779 80.902 
-0.000  53.000 
58.779  80.902 
50.406  16.378 
95.106  -30.902
31.153  -42.878
      z"/>
 </g>
</svg>

最初のSVGファイルなので,細かく解説しましょう. 初めの行で,XMLを表します. 2〜3行目は,DOCTYPE宣言です. これらは,他のSVGファイルを作るときも,同じ記述です.

「svgタグ」で始まる4行目からが,SVGの定義部分です. ここでは,描画位置を,(-200,-200)が左上, (-200+400,-200+400)すなわち(200,200)が右下となるような, サイズが400x400の長方形領域とします. 原点を中心としたいので,こうしていますが,もし原点が左上でこのサイズにするなら,
<svg width="400" height="400" viewBox="0 0 400 400">
と記述します.

5行目の「gタグ」の中身は, このgタグに囲まれたところ(<g …>なにがし</g>の「なにがし」)では,(0,16)を原点としさらに2倍に拡大して描画し, 塗りつぶしは行わず, 線画の色は黒,線の太さは5ピクセル,線のつながりに丸みを持たせるよう指示しています. 原点やら拡大やらが出てきますが,これは,論理座標系と物理座標系(あるいは,描画座標系)という概念を用いるとわかりやすくなるでしょうか. 論理座標系とは,gタグ内,上述の「なにがし」のところで点を指定する方法です. しかしその値のまま点をとって描画するのではなく,描画の際に「(0,16)を原点としさらに2倍に拡大」するという処理を行います. 例えば…このファイルの中に「-31.153 -42.878」という値がありますが, (-31.153,-42.878)がその論理座標系の座標で, 描画座標系に変換すると,(-31.153*2,(-42.878+16)*2),すなわち (-62.306,-53.756)を指定したことになります.

7行目から17行目までが,点をとっていく「pathタグ」です. 17行目の最後を「/>」とすることで,</path>が不要になるのはXMLのルールですね.

8行目から17行目までの,ダブルクオートで囲まれた部分で, どの点をとっていくかを指定します. 最初に「M」が直後のふたつの数値「0.000 -100.000」をとり,始点を指定します. 次に「L」と「-31.153 -42.878」とで,次に移動する点の絶対座標を表します. 絶対座標と別に相対座標による指定もできますが,この使い分けについては, 別の機会に解説したいと思っています. さて,その後は2個1組の数値ばかりが並びますが,これは「L」がとるパラメータと なります.正確に言うと,Lは偶数個の数値をパラメータにとり, 2個1組で順番に,次に移動する点の絶対座標を指定していきます. 最後は「z"」です.「"」は文字列のおしまい.「z」は今までとっていった点の 始点と終点を結べという指示です.「z」は「Z」でもかまいません. しかし「L」を「l」にすると,全然違う絵になります. また,このファイルの場合「M」を「m」にしても同じ絵になりますが, これらの意味は微妙に違うので,このファイルでは「M」のほうが適切です.

MやLのように数値列をとるコマンドでは,数値列を空白文字もしくはカンマ(,)で 区切ります.改行も空白文字とみなされるので, 上のファイルの書き方で文法上は問題ありませんが, もし気持ち悪いと感じるなら,清書の際に
<path d="M0.000,-100.000 L-31.153,-42.878 -95.106,-30.902 -50.406,16.378 -58.779,80.902 -0.000,53.000 58.779,80.902 50.406,16.378 95.106,-30.902 31.153,-42.878 z"/>
とするのがいいかもしれません.

◆ ◆ できあがり ◆ ◆

画像はこうなります.

ファイル名: star03.svg
上のほうでお見せした星型よりも,ふっくらしていることを確かめてください.

◆ ◆ バリエーション ◆ ◆

その「ふっくら」の違いは,中心から近いほうの点の距離…中心から 遠いほうの点の距離を100とした,近いほうの点の相対距離a…にあります. この距離を小さくすれば尖った星型になり,大きくすれば丸くなります.

極端な話,0にすると,正五角形の各頂点と中心を結んだものになり,

ファイル名: star04.svg
遠いほうの点と同じ(a=100)にすると,正十角形になります.
ファイル名: star05.svg
さらに,aの値を負にすることで,こんな図形も描けます.
ファイル名: star06.svg

これらの座標を求めるのには,先ほど作ったプログラムを改造します. 具体的には,こうです.
ファイル名: calc01.rb
#!/usr/bin/env ruby

h1 = 100.0
h2 = (ARGV.shift || 53.0).to_f

def my_sin(angle)
  Math::sin(angle.to_f * Math::PI / 180.0)
end

def my_cos(angle)
  Math::cos(angle.to_f * Math::PI / 180.0)
end

0.upto(9) do |i|
  if i % 2 == 0 then r = h1 else r = h2 end
  theta = 90 + i * 36
  printf "%-7.3f %-7.3f\n", r * my_cos(theta), -r * my_sin(theta)
end
このプログラムのポイントは(そして改造点となるのは),
h2 = (ARGV.shift || 53.0).to_f
のところです.これは,実行時に引数があればそれ(を浮動小数点数にしたもの)を, 引数がなければ53.0を,相対距離aの値にしなさい,という意味です.

そうしてあとは,
[プロンプト] ruby calc01.rb 0
なり
[プロンプト] ruby calc01.rb 100
なり
[プロンプト] ruby calc01.rb -50
なりを実行して,その出力を,先ほどのSVGファイルに張り替えるだけです. ペーストしてから,MとLを書き加えるのを忘れてはいけませんね.

◆ ◆ 最初の星型について ◆ ◆

おしまいに,最初に掲げた星型の描き方について,触れておきましょう. これです.

ファイル名: star01.svg

この場合の相対距離aはいくらになるかですが, 正五角形に対角線を引いて,原点から補助線を引くと…ああ,いい図があるじゃないですか.

ファイル名: star07.svg
この図で,太い黒線で描いた三角形に着目すると, 赤の線分の長さを100としたときの,青の線分の長さが求められます. (この三角形の上の頂点の角度は18度,下の頂点の角度は36度になります.そこからは,高校で教わる三角比を使った計算です.) この長さは,
100*tan(18)/((tan(36)+tan(18))*cos(36))
となります. このままではSVGに乗せられませんが, この値を求めるプログラムを作ればいいわけで,やはりRubyで書きましょう.
ファイル名: calc02.rb
#!/usr/bin/env ruby

def my_cos(angle)
  Math::cos(angle.to_f * Math::PI / 180.0)
end

def my_tan(angle)
  Math::tan(angle.to_f * Math::PI / 180.0)
end

puts 100 * my_tan(18) / ((my_tan(36) + my_tan(18)) * my_cos(36))
出力には,printfではなく,putsを使用しています. これは,引数ごと(ここでの引数はひとつですが)に評価した値と改行文字を 出力するという関数です.

実行結果はこうです.
[プロンプト] ruby calc02.rb
38.19660113
これを用いて,
[プロンプト] ruby calc01.rb `ruby calc02.rb`
0.000   -100.000
-22.451 -30.902
-95.106 -30.902
-36.327 11.803 
-58.779 80.902 
-0.000  38.197 
58.779  80.902 
36.327  11.803 
95.106  -30.902
22.451  -30.902
とすると,星型を描くのに必要な座標が出てきます.

◆ あとがき ◆

この文書では,星型の描き方を通して, SVGで「数学的に整った」画像を作るひとつの方法を紹介しました. SVGのメリットは,冒頭で述べたこと以外にもいくつかあります. 一例を挙げると,gタグでグループ化することにより, 画像要素を構造的に配置することができる点です. これはペイントツールのレイヤの概念と似ています. (そして,SVGの仕様では,レイヤの段数に制限がありません.) このことと,SVGファイルはXMLファイルであることから, 画像要素の再利用がしやすいことが期待できます.

SVGと似た画像フォーマットに,EPSというのがあります. 実際,SVGのpathタグや塗りつぶしの仕様を見たとき,EPSの基礎となっている PostScriptを真似ているんじゃないの,真似すぎだよこれ,という印象を持ちました. SVGもEPSも,テキストファイルであること, ファイル中に画像サイズの指定がなされていることなどの共通点がありますが, EPSではファイル中に式を書ける(PostScriptはプログラミング言語なのです)こと, EPSの主な用途は印刷であるのに対してSVGはWebでの閲覧であること, といった違いもあります. 今後は,EPSで記述されたコードをSVGに取り込みながら実用的な絵を描くという, また新たな「星の描き方」についてとりまとめることにしています.


zmp's
zmp@s1.xrea.com
Last modified: Fri Jun 07 21:56:34 JST 2002