The Basic Setup
I spent three weeks last semester debugging why my lunar phase calculator kept showing a waxing crescent when it should have been waning gibbous. The issue wasn't in the math — it was in how I handled the time zone conversion before passing the Julian date into the phase function. That's the kind of thing that eats your time if you're not careful. The Science Project Moon Phases work involves computing where the Moon is relative to the Sun from Earth's perspective. Most people use a simplified algorithm based on the synodic month, which is about 29.53059 days. You feed it a date and get back a number between 0 and 1 representing how far through the current cycle you are. Multiply that by eight and you get the approximate phase name. It's not glamorous but it works for school projects and lightweight apps.
Using Science Project Moon Phases for Your Own Calculator
Here's the core loop I ended up relying on after my first attempt failed. You compute the days since a known new moon, divide by the synodic period, take the fractional part, and map it to phases. The tricky part is choosing your epoch right. If you pick something arbitrary like January 1st year zero, your error accumulates fast because you're doing floating point math on a huge number. Pick a recent new moon as your reference point and you stay accurate for years without drift. I found that using the NASA Lunar Phase algorithm as a reference gave me results within one degree of their ephemeris data for dates between 2000 and 2100. That's good enough for pretty much any classroom project. The formula looks like this: calculate the Julian date from your input, subtract the Julian date of the reference new moon, divide by 29.53058867, take modulo 1, then map 0 to new, 0.25 to first quarter, 0.5 to full, and 0.75 to last quarter. Anything between gets interpolated. One thing nobody tells you: the Moon doesn't actually spend equal time in each phase name if you define phases by exact angle. The orbital speed varies because the orbit is elliptical. A waxing gibbous phase can last noticeably longer than a waning crescent depending on where perigee falls in the cycle. If your project needs to be precise about timing rather than just visual appearance, you need to account for this with Kepler's equation or at least a simple eccentricity correction term.
Another pitfall I ran into: timezone handling. If you're computing phases for an audience in multiple time zones, the phase name can flip midnight to midnight across zones even though astronomically nothing changed. I solved this by storing and displaying phases in UTC and letting the UI convert for local display only. That way "full moon tonight" means the same thing regardless of whether someone is in London or Tokyo. For rendering the visual phase, most students reach for CSS masks or pre-made SVG templates. I built mine using canvas drawing with a simple shadow calculation. The terminator line on the Moon isn't a perfect semicircle except at quarter phases. For intermediate phases, you need to draw an elliptical shadow whose curvature changes with the phase angle. The math is straightforward — the shadow edge is an ellipse with semi-minor axis equal to the cosine of the phase angle times the radius. It takes about twenty lines of code and looks noticeably better than the basic circle-subtract-circle approach. Here's a minimal implementation I used as a starting point:
Get the Full Details

function getMoonPhase(date) { const epoch = new Date('2000-01-06T18:14:00Z'); const days = (date - epoch) / 86400000; const cycle = 29.53058867; const phase = ((days % cycle) + cycle) % cycle / cycle; return phase; } This returns a value from 0 to 1. Map it however you need. For a science fair display, I added a small lookup table that gave each phase a two-word name and a hex color for the background. Students tended to gravitate toward the visual output more than the underlying math, so making it look decent mattered more than making it astronomically perfect. If you need higher accuracy, there are libraries like ephem or skyfield that implement full orbital mechanics. They're overkill for a project that just needs to show "it's a waning crescent" but necessary if you're calculating exact rise and set times or comparing against observational data. My personal benchmark was matching against an online almanac to within four minutes of transit time, which required switching to a proper atmospheric refraction model near the horizon.
Common Mistakes
The biggest one I see is mixing up the sidereal month with the synodic month. The sidereal period is about 27.3 days and describes the Moon's position relative to the stars. The synodic period is what determines phases and is longer because Earth is also moving around the Sun. Using the wrong one throws off your entire cycle by roughly two days. Another mistake is not handling negative remainders properly in languages where the modulo operator preserves the sign of the dividend. JavaScript's % operator will give you a negative result if your input date is before the epoch. You need to add the cycle length before taking modulo, or use a proper modulo function. I wasted an afternoon chasing a bug where dates before January 2000 showed completely wrong phases because of this. For the visual representation, a common shortcut is to use a simple circle with a semi-circle cut out for all phases. This looks roughly correct at quarters but becomes obviously wrong at crescent and gibbous stages because the terminator curvature is wrong. The fix is using the ellipse method I described above. It's not much more code and it makes the difference immediately noticeable.
If your project involves tracking the Moon over multiple months, you'll notice that the phase calendar shifts earlier by about eleven days each year. This is because the common year is roughly thirty-five days longer than twelve synodic months. Accounting for this drift in your display logic helps people understand why Full Moon dates move around the calendar rather than staying fixed.

Where This Approach Breaks Down
The simple algorithm I described works well for dates within a century of the epoch but starts accumulating error beyond that. For historical dates going back thousands of years, you need to account for tidal acceleration, which is slowly lengthening the synodic month by about 2.3 milliseconds per century. Over millennia this adds up to several hours of phase timing error. If your project involves ancient eclipse dating or historical astronomical records, the simple method won't cut it. The approach also doesn't account for parallax. If you're trying to predict exactly what phase an observer at a specific location on Earth will see at a specific moment, the Moon's proximity means that observers on opposite sides of the planet see slightly different illuminations. This effect is small — a few percent at most — but it's real and measurable with good equipment. For a classroom project it's negligible. For anything requiring observational accuracy, you need topocentric calculations. I ended up wrapping my calculator in a simple web interface using vanilla JavaScript so students could test it without installing anything. The whole thing was about two hundred lines including the rendering code. It ran fine in any modern browser and took roughly three seconds to load on a slow connection, which mattered because some of the schools I shared it with had outdated hardware.
If you want to go further, adding a simple star field background and showing the Moon's position relative to nearby constellations makes the project feel more complete. The constellation data is public domain and you can find it in standard star catalog formats. Mapping phase to sky position requires a separate set of calculations involving the Moon's ecliptic longitude, but once you have that, the visual presentation is noticeably more impressive to judges and teachers alike. The source code for my final version is available on GitHub under the MIT license. I documented the assumptions and error bounds in the README so other people could adapt it without reinventing the error analysis. Most forks I saw improved the rendering but left the calculation logic unchanged, which told me the algorithm itself was solid even if the presentation varied.