【SVG】月の満ち欠けを月齢ごとに描く方法【JavaScript】
「Webサイトで月の満ち欠けを表示させたい」
「月の満ち欠けの画像を30種類つくって表示するよりもっと効率的な方法はない?」
こんな悩みにお答えします。
じつは、SVGとJavaScriptを使った図形をつくることで動的な月の満ち欠けを描くことができます。
この記事では、以下の解説をしています。
- SVGを使って月を描く方法
- SVG、JavaScriptで月を描くメリット
- SVG、JavaScriptで月を描くデメリット
記事を読み終えると、WebサイトにSVGを使った動的な月をつくることができ、月齢ごとに多くの月の画像を用意する必要がなくなります。
- 目次
SVG、JavaScriptで月の満ち欠けを月齢ごとに描く方法
SVGのcircleタグで完全な円をつくってしまうと、三日月、半月などをうまく描けなくなるので、pathタグを使い2本の線のY軸の中間点のX軸が変化するようにします。
この機能をつくっていきます。
HTML(SVG)を記述する
最低限、svgタグだけ書いておきます。
pathはJavaScriptで追加していくので、面倒な場合は省略してもいいです。
<svg class="moon" data-moon-age="0"></svg>
JavaScriptで動的な操作が必要ない場合は、以下だけで満月(円)になります。
※月齢15にしていますが、厳密には15だと満月を超える状態になります。
<svg class="moon" data-moon-age="15" width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <rect width="200" height="200" fill="black"></rect> <path d=" M 100,0 A 96.82084678198865,100 0 1,1 100,200 A 100,100 0 0,1 100,0 Z " fill="yellow"></path> </svg>
複数の月を表示する場合、月の画像を使いたい場合は、symbolタグを使って画像を読み込んだSVGタグ、defsタグを追加し、fillで画像を指定したSVGタグがを書きます。
これだと画像を複数のSVGに使いまわすことができます。
※「moon.png」は環境にあわせてパスを変更してください
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> <symbol id="moonSymbol" viewBox="0 0 200 200"> <image href="moon.png" width="200" height="200" /> </symbol> </svg> <svg class="moon" data-moon-age="15" width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <defs> <pattern id="moonPattern" patternUnits="userSpaceOnUse" width="200" height="200"> <use href="#moonSymbol"></use> </pattern> </defs> <rect width="200" height="200" fill="black"></rect> <path d=" M 100,0 A 96.82084678198865,100 0 1,1 100,200 A 100,100 0 0,1 100,0 Z " fill="url(#moonPattern)"></path> </svg>
JavaScriptを記述する
<script> function updateMoonPhases() { const moons = document.querySelectorAll(".moon"); moons.forEach(moon => { // 月齢の周期 const cycle = 29.53058867; const moonAge = parseFloat(moon.getAttribute("data-moon-age")); // SVG(.moon)に属性を追加 moon.setAttribute("width", 200); moon.setAttribute("height", 200); moon.setAttribute("viewBox", "0 0 200 200"); moon.setAttribute("xmlns", "http://www.w3.org/2000/svg"); // SVG(.moon)にdefsタグを追加 let defs = moon.querySelector("defs"); if (!defs) { defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); moon.appendChild(defs); pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern"); defs.appendChild(pattern); pattern.setAttribute("id", "moonPattern"); pattern.setAttribute("patternUnits", "userSpaceOnUse"); pattern.setAttribute("width", 200); pattern.setAttribute("height", 200); use = document.createElementNS("http://www.w3.org/2000/svg", "use"); pattern.appendChild(use); use.setAttribute("href", "#moonSymbol"); } // SVG(.moon)にrectタグを追加 let rect = moon.querySelector("rect"); if (!rect) { rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); moon.appendChild(rect); rect.setAttribute("width", 200); rect.setAttribute("height", 200); rect.setAttribute("fill", "black"); } // moonAgeがありえない数値(月齢)の場合はスキップ if( moonAge < 0 || moonAge > cycle ){ return; } // SVG(.moon)にpathタグを追加 let moonPath = moon.querySelector("path"); if (!moonPath) { moonPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); moon.appendChild(moonPath); } // 月齢ごとに月の満ち欠けを定義 let age; let d1 = {}; let d2 = {}; const quarter = cycle / 4; if (moonAge < quarter) { age = 100 - (moonAge / quarter) * 100; d1 = { rx: 100, large_arc_flag: 1, sweep_flag: 1 }; d2 = { rx: age, large_arc_flag: 1, sweep_flag: 0 }; } else if (moonAge < quarter * 2) { age = ((moonAge - quarter) / quarter) * 100; d1 = { rx: 100, large_arc_flag: 1, sweep_flag: 1 }; d2 = { rx: age, large_arc_flag: 0, sweep_flag: 1 }; } else if (moonAge < quarter * 3) { age = 100 - ((moonAge - 2 * quarter) / quarter) * 100; d1 = { rx: age, large_arc_flag: 1, sweep_flag: 1 }; d2 = { rx: 100, large_arc_flag: 0, sweep_flag: 1 }; } else { age = ((moonAge - 3 * quarter) / quarter) * 100; d1 = { rx: age, large_arc_flag: 1, sweep_flag: 0 }; d2 = { rx: 100, large_arc_flag: 0, sweep_flag: 1 }; } const newPath = ` M 100,0 A ${d1.rx},100 0 ${d1.large_arc_flag},${d1.sweep_flag} 100,200 A ${d2.rx},100 0 ${d2.large_arc_flag},${d2.sweep_flag} 100,0 Z `; moonPath.setAttribute("d", newPath); moonPath.setAttribute("fill", "url(#moonPattern)"); // moonPath.setAttribute("fill", "yellow"); }); } window.onload = updateMoonPhases; </script>
SVG、JavaScriptで月を描くメリット
月の画像をいくつも用意する必要がない
月齢ごとの月を表示させたカレンダーなど30種類の画像を用意しているサイトが多いですが、JavaScriptを使ってSVGのpathを動的に操作するので月の画像をいくつも用意する必要がないです。
レスポンシブ(サイズの大小)にも対応できる
月にかぎらずSVGはベクター画像なので、サイズに合わせた画像を用意する必要がなく、ぼやけたりすることがありません。
PC、タブレット、スマホも1つのファイルだけで大丈夫です。
より細かな月齢に対応できる
月齢をもとにプログラムした月を描くため、月齢が小数点以下の場合でも対応することができます。
あまり高精細な満ち欠けが必要になることは少ないとは思いますが、対応することは可能です。
SVGファイルなので自由に操作できる(色やアニメーションなど)
SVGファイルだと、ある程度知識があれば、JavaScript、CSSを使って、色やアニメーションの操作をすることができます。
jpg、pngなどの画像だと切り出した画像をあとから操作することができません。
SVG、JavaScriptで月を描くデメリット
SVGのデメリットはあまりありませんが、しいてあげるとすればSVGに対応していない古いブラウザでは使えないということがあります。
IE10とかIE9とかであれば、その仕様でWebサイトを制作すること自体がもうほぼ無いので、あまり気にする必要はないと思います。
まとめ
SVG、JavaScriptを使って月の満ち欠けを月齢ごとに描く方法を解説しました。
SVGで月の満ち欠けを描くことで以下のようなメリットがあります。
- 月の画像をいくつも用意する必要がない
- レスポンシブ(サイズの大小)にも対応できる
- より細かな月齢に対応できる
- SVGファイルなので自由に操作できる(色やアニメーションなど)
SVGで月の満ち欠けを描くことで多くの画像を用意する必要がなくなります。
また、より細かな単位の月齢まで対応した月の満ち欠けをWebサイトに表示することが可能になります。