Search code examples
javascriptsvgastronomy

SVG Moon Phases


For a larger project, I'd like to add the moon phase on a hover date. I found javascript that calculates the moon phase and I also found the SVG moon phase solution posted by @Alexandr_TT at Create waxing crescent moon in svg using path.

The javascript mathematically calculates the moon phase:

function getMoonPhase(year, month, day) {
    var c = e = jd = b = 0;

    if (month < 3) {
    year--;
    month += 12;
    }
    ++month;
    c = 365.25 * year;
    e = 30.6 * month;
    jd = c + e + day - 694039.09; //jd is total days elapsed
    jd /= 29.5305882; //divide by the moon cycle
    b = parseInt(jd); //int(jd) -> b, take integer part of jd
    jd -= b; //subtract integer part to leave fractional part of original jd
    b = Math.round(jd * 8); //scale fraction from 0-8 and round
    if (b >= 8 ) {
        b = 0; //0 and 8 are the same so turn 8 into 0
    }

    // 0 => New Moon
    // 1 => Waxing Crescent Moon
    // 2 => Quarter Moon
    // 3 => Waxing Gibbous Moon
    // 4 => Full Moon
    // 5 => Waning Gibbous Moon
    // 6 => Last Quarter Moon
    // 7 => Waning Crescent Moon

    return b;
}

Alexandr's SVG is an animation, so my question is can the svg be a snapshot based on the output value of the javascript?

I can certainly generate separate static SVG's for each of the 8 phases. I could also try and modify the solution I found on Github at https://github.com/tingletech/moon-phase, but I am attracted to Alexandr's SVC code because of it's small size.

My goal would be to output the numeric value of the javascript that would then render a visually actuate SVG, in the least amount of code needed to accomplish the goal.

I'm a noob on SVG. The solution on github uses the arc attribute of path in SVG, and it looks like Alexandr is using two intersecting circles. Can the animation be effectively stopped based on the value I get from the javascript?

Thanks in advance.


Solution

  • Hi I have a working copy such as this:

    <!DOCTYPE html>
    <html lang="en" dir="ltr">
    <head>
    	<meta charset="utf-8">
    	<title>Moon Phase Today</title>
    </head>
    <body>
    	<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"  viewBox="0 0 73 73" >
    		<defs>
    			<radialGradient id="RadialGrad" fx="50%" fy="50%" r="65%" spreadMethod="pad">
    				<stop offset="0%"   stop-color="#E7D68C" stop-opacity="1"/>
    				<stop offset="100%" stop-color="#FFFEED" stop-opacity="1" />
    			</radialGradient>
    			
    		</defs>
    		<rect width="100%" height="100%" />
    		<g transform="rotate(-20 35.5 35.5)">
    			<circle cx="35.5" cy="35.5" r="35" stroke="none"  fill="url(#RadialGrad)" />
    			
    			<circle id='layoverCircle' cx="35.5" cy="35.5" r="35" stroke="none" fill="black" />
    			
    			<rect id="layoverRectangle" style="display: none" width="100%" height="100%" />
    				<!-- <animate id="youngMoon" attributeName="cx" values="35.5;-35.5;" begin="1s;oldMoon.end+1s" dur="10s" fill="freeze" /> -->
    				<!-- <animate id="oldMoon" attributeName="cx" values="105;35.5;" begin="youngMoon.end+1s" dur="10s"  fill="freeze" />  -->
    				
    			<!-- </circle>  -->
    		</g>
    	</svg>
    	<script type="text/javascript">
    		function setState(value, showCircle, showRect) {
    			
    			let circle = document.getElementById('layoverCircle');
    			let rect = document.getElementById('layoverRectangle');
    			
    			circle.style.display = showCircle ? "block" : "none";
    			rect.style.display = showRect ? "block" : "none";
    			
    			if (showRect) rect.style.transform = value
    			if (showCircle) circle.setAttribute("cx", value);
    		}
    		
    		function getMoonPhase(year, month, day) {
    			var c = e = jd = b = 0;
    			
    			if (month < 3) {
    				year--;
    				month += 12;
    			}
    			++month;
    			c = 365.25 * year;
    			e = 30.6 * month;
    			jd = c + e + day - 694039.09; //jd is total days elapsed
    			jd /= 29.5305882; //divide by the moon cycle
    			b = parseInt(jd); //int(jd) -> b, take integer part of jd
    			jd -= b; //subtract integer part to leave fractional part of original jd
    			b = Math.round(jd * 8); //scale fraction from 0-8 and round
    			if (b >= 8 ) {
    				b = 0; //0 and 8 are the same so turn 8 into 0
    			}
    			
    			
    			// 0 => New Moon 37.5 [show circle, hide rect]
    			// 1 => Waxing Crescent Moon 50.5 [show circle, hide rect]
    			// 2 => Quarter Moon //translateX(50%), [display rect, hide circle]
    			// 3 => Waxing Gibbous Moon 70.5 [show circle, hide rect]
    			// 4 => Full Moon [hide circle and rect]
    			// 5 => Waning Gibbous Moon, -15.5 [show circle, hide rect]
    			// 6 => Last Quarter Moon //transform: translateX(-50%) [display rect, hide circle]
    			// 7 => Waning Crescent Moon 30.5 [show circle, hide rect]
    			
    			return b;
    		}
    		
    		let d = new Date();
    		let i = 0;
    		let callback = function () {			
    			let phase = getMoonPhase(d.getFullYear(), d.getMonth()+1, d.getDate() + i)
    			i += 4
    			if (phase == 0) setState(37.5, true, false);
    			if (phase == 1) setState(50.5, true, false);
    			if (phase == 2) setState("translateX(50%)", false, true);
    			if (phase == 3) setState(70.5 , true, false);
    			if (phase == 4) setState(NaN , false, false);
    			if (phase == 5) setState(-15.5, true, false);
    			if (phase == 6) setState("translateX(-50%)", false, true);
    			if (phase == 7) setState(30.5 , true, false);
    			setTimeout(callback, 1000);
    		};
    		callback();
    	</script>
    </body>
    </html>

    setState function is basically what you are looking for. You can also change values in setState to your esthetical needs.