Trying to visualize the tables at https://www.timeanddate.com/moon/ to better see where the moon is from day to day.
A Pen by Andreas Borgen on CodePen.
| <script src="//unpkg.com/[email protected]"></script> | |
| <script src="//unpkg.com/[email protected]"></script> | |
| <script src="//unpkg.com/[email protected]"></script> | |
| <header> | |
| <h2>Moon calendar</h2> | |
| <aside>(All data from <a target="_blank" href="https://www.timeanddate.com/moon/norway/hamar?month=9&year=2018">timeanddate.com</a> )</aside> | |
| </header> | |
| <form> | |
| <div class="location"> | |
| <label> | |
| <span class="caption">Country</span> | |
| <input type="text" name="country" value="Norway" /> | |
| </label> | |
| <label> | |
| <span class="caption">City</span> | |
| <input type="text" name="city" value="Hamar" /> | |
| </label> | |
| </div> | |
| <div class="month-picker"> | |
| <div class="year"> | |
| <label> | |
| <span class="caption">Year</span> | |
| <input type="number" name="year" value="2018" /> | |
| </label> | |
| </div> | |
| <div class="month"> | |
| <span class="caption">Month</span> | |
| <div class="inputs"> | |
| <label><input type="radio" name="month" value="1" /><span>Jan</span></label> | |
| <label><input type="radio" name="month" value="2" /><span>Feb</span></label> | |
| <label><input type="radio" name="month" value="3" /><span>Mar</span></label> | |
| <label><input type="radio" name="month" value="4" /><span>Apr</span></label> | |
| <label><input type="radio" name="month" value="5" /><span>May</span></label> | |
| <label><input type="radio" name="month" value="6" /><span>Jun</span></label> | |
| <label><input type="radio" name="month" value="7" /><span>Jul</span></label> | |
| <label><input type="radio" name="month" value="8" /><span>Aug</span></label> | |
| <label><input type="radio" name="month" value="9" /><span>Sep</span></label> | |
| <label><input type="radio" name="month" value="10"/><span>Oct</span></label> | |
| <label><input type="radio" name="month" value="11"/><span>Nov</span></label> | |
| <label><input type="radio" name="month" value="12"/><span>Dec</span></label> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <button type="submit">Update</button> | |
| </div> | |
| </form> | |
| <div id="container"></div> | |
| <div id="shortcuts"> | |
| <a id="start" href="#">First day of month</a> | |
| <a id="now" href="#" style="display:none;">Today</a> | |
| <a id="end" href="#">Last day of month</a> | |
| </div> | |
| <div id="loader"></div> |
Trying to visualize the tables at https://www.timeanddate.com/moon/ to better see where the moon is from day to day.
A Pen by Andreas Borgen on CodePen.
| console.clear(); | |
| /** | |
| * localForage + lz-string | |
| * https://github.com/localForage/localForage | |
| * https://github.com/pieroxy/lz-string | |
| */ | |
| class CompressedForage { | |
| constructor(key) { | |
| this._key = key; | |
| this.key = 'ABO_CompressedForage_' + key; | |
| } | |
| _onError(err) { | |
| console.error(err); | |
| } | |
| _containerWork(work, callback) { | |
| const mainKey = this.key, | |
| errHandler = this._onError; | |
| localforage.getItem(mainKey) | |
| .then(function(cont) { | |
| if(!cont) { cont = {}; } | |
| const result = work(cont); | |
| localforage.setItem(mainKey, cont) | |
| .then(function(cont) { | |
| if(callback) { callback(result); } | |
| }) | |
| .catch(errHandler); | |
| }) | |
| .catch(errHandler); | |
| } | |
| _normKey(key) { | |
| //See points 2 and 3 here (avoid overriding standard functions and properties on Object): | |
| //http://2ality.com/2012/01/objects-as-maps.html | |
| return '_@' + key; | |
| } | |
| setItem(key, data, callback) { | |
| const k = this._normKey(key); | |
| this._containerWork(cont => { | |
| const buffer = data ? LZString.compressToUint8Array(data) : data; | |
| cont[k] = buffer; | |
| }, callback); | |
| } | |
| getKeys(callback) { | |
| this._containerWork(cont => { | |
| const keys = Object.keys(cont).map(k => { | |
| //Keep this code synced with _normKey().. | |
| return k.substring(2); | |
| }); | |
| return keys; | |
| }, callback); | |
| } | |
| getItem(key, callback) { | |
| const k = this._normKey(key); | |
| this._containerWork(cont => { | |
| const buffer = cont[k], | |
| data = buffer ? LZString.decompressFromUint8Array(buffer) : buffer; | |
| return data; | |
| }, callback); | |
| } | |
| removeItem(key, callback) { | |
| const k = this._normKey(key); | |
| this._containerWork(cont => { | |
| //https://stackoverflow.com/questions/208105/how-do-i-remove-a-property-from-a-javascript-object | |
| //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete | |
| delete cont[k]; | |
| }, callback); | |
| } | |
| clear(callback) { | |
| localforage.removeItem(this.key) | |
| .then(callback) | |
| .catch(this._onError); | |
| } | |
| } | |
| function createTimeline(table) { | |
| const ud = ABOUtils.DOM, | |
| [$, $$] = ud.selectors(); | |
| let events = []; | |
| function parseEvent(type, date, time, deg) { | |
| if(!time) { return null; } | |
| function extractNumbers(str) { | |
| return str.match(/(\d+)/g).map(Number); | |
| } | |
| function parseTime(time) { | |
| time = time.trim(); | |
| let [h, m] = extractNumbers(time); | |
| //12:13 am -> 00:13 | |
| // 1:16 am -> 01:16 | |
| //.. | |
| //12:42 pm -> 12:42 | |
| // 1:52 pm -> 13:52 | |
| if(time.indexOf('am') > 0) { | |
| if(h === 12) { h = 0; } | |
| } | |
| else if(time.indexOf('pm') > 0) { | |
| if(h !== 12) { h += 12; } | |
| } | |
| return [h, m]; | |
| } | |
| //Example: "↑ (296°)", "13:14" | |
| const degr = extractNumbers(deg)[0], | |
| [h, m] = parseTime(time), | |
| dayFraction = (h + (m/60))/24, | |
| monthIndex = date-1 + dayFraction; | |
| const event = createEvent(type, monthIndex, degr); | |
| event.raw = { date, time, deg }; | |
| return event; | |
| } | |
| function createEvent(type, time, degr) { | |
| return { | |
| type, | |
| time, | |
| degr, | |
| }; | |
| } | |
| $$('tr[data-day]', table).forEach(tr => { | |
| const date = Number(tr.dataset.day), | |
| tds = $$('td', tr); | |
| //Account for empty padder cells: | |
| const cells = []; | |
| tds.forEach(td => { | |
| let span = td.colSpan; | |
| if(span === 1) { | |
| cells.push(td.textContent.trim()); | |
| } | |
| else { | |
| while((span--) > 0) { cells.push(''); } | |
| } | |
| }); | |
| let up1 = parseEvent('up', date, cells[0], cells[1]), | |
| down = parseEvent('down', date, cells[2], cells[3]), | |
| up2 = parseEvent('up', date, cells[4], cells[5]), | |
| max = parseEvent('max', date, cells[6], cells[7]); | |
| if(max) { | |
| max.illum = parseFloat(cells[9]); | |
| } | |
| events.push(up1, down, up2, max); | |
| }); | |
| //Order the events into a moon's rise, max and set: | |
| events = events.filter(x => x).sort((a, b) => a.time - b.time); | |
| //Fake first moonrise and last moonset if we have a max: | |
| const e1 = events[0], | |
| e2 = events[1], | |
| e99 = events[events.length - 2], | |
| e100 = events[events.length - 1]; | |
| if(e1.type === 'max') { | |
| const up0 = createEvent('up', e1.time - (e2.time - e1.time)); | |
| events.unshift(up0); | |
| } | |
| if(e100.type === 'max') { | |
| const down101 = createEvent('down', e100.time + (e100.time - e99.time)); | |
| } | |
| while(events[0].type !== 'up') { events.shift(); } | |
| //console.log('Events: ', events); | |
| //Group the events into a moon's rise, max and set: | |
| const moons = []; | |
| for(let i = 2; i < events.length; i += 3) { | |
| if(events[i].type !== 'down') { throw 'Order WTF?! - ' + i; } | |
| moons.push({ | |
| up: events[i-2], | |
| max: events[i-1], | |
| down: events[i], | |
| }); | |
| } | |
| console.log('Moons:', moons); | |
| /* Render the "calendar" */ | |
| const chart = ud.createElement('.moon-chart'), | |
| base = ud.createElement('.day-base', chart); | |
| function toBasePct(x) { | |
| return (x * 100).toFixed(1) + '%'; | |
| } | |
| function toBase(x) { | |
| //console.log(x, base.clientWidth); | |
| return Math.round(x * base.clientWidth); | |
| } | |
| /* Not quite reliable.. | |
| function moonPos(m) { | |
| function isEast (degr) { return (degr < 180); } | |
| function isSouth(degr) { return (degr > 90) && (degr < 270); } | |
| function posInfo(degr) { | |
| if(!degr) { return null; } | |
| return { | |
| degr: degr, | |
| isEast: isEast(degr), | |
| isSouth: isSouth(degr), | |
| }; | |
| } | |
| //The moon rotates very slowly around the earth (.56 degrees/hr), | |
| //so its perceived motion across the sky is caused by earth's rotation around itself (15 degrees/hr). | |
| //We see it moving from east to west. Far north, it is always to the south, and vice versa. | |
| //https://www.quora.com/Does-the-moon-orbit-the-earth-at-the-equator-Does-the-orbit-vary-much | |
| //https://cseligman.com/text/sky/moonmotion.htm | |
| const p1 = posInfo(m.up.degr), | |
| p2 = posInfo(m.down.degr); | |
| if(!p1 || !p2) { return '?'; } | |
| let makesSense = true; | |
| if(p2.isEast || !p1.isEast) { | |
| makesSense = false; | |
| } | |
| if(p1.isSouth !== p2.isSouth) { | |
| makesSense = (m.max.degr > 80); | |
| } | |
| if(!makesSense) { throw 'Pos WTF?!'; } | |
| const hoursUp = (m.down.time - m.up.time) * 24, | |
| toSouth = (hoursUp < 12) ? p1.isSouth : !p1.isSouth; | |
| const degsTraveled = toSouth ? (p2.degr - p1.degr) : (p1.degr + 360-p2.degr), | |
| speed = degsTraveled/hoursUp, | |
| centerDegr = toSouth ? (p2.degr + p1.degr)/2 : p1.degr - (p1.degr + 360-p2.degr)/2; | |
| console.log(`pos ${m.max.raw.date} ${toSouth ? 'S' : 'N'} ${hoursUp.toFixed(1)}h\t(${centerDegr}°\t${speed.toFixed(1)}°/h)`); | |
| return centerDegr; | |
| } | |
| */ | |
| moons.forEach((m, i) => { | |
| const illum1 = i ? moons[i-1].max.illum : m.max.illum, | |
| illum2 = i ? m.max.illum : moons[i+1].max.illum; | |
| m.wane = illum1 > illum2; | |
| const path = ud.createElement('svg.moon-path', base, { | |
| style: `left:${toBasePct(m.up.time)};`, //width:${toBasePct(m.down.time - m.up.time)}; height:${toBasePct(2 * m.max.degr/90)};` | |
| width: toBasePct(m.down.time - m.up.time), | |
| height: toBasePct(m.max.degr/90), | |
| viewBox: '0,0 100,50', | |
| preserveAspectRatio: 'none', | |
| }); | |
| ud.createElement('path', path, { | |
| d: 'M0,50 Q50,-50 100,50', | |
| }); | |
| ud.createElement('.east-west-marker', base, { | |
| style: `left:${toBasePct(m.max.time)}; height:${toBasePct(m.max.degr/90)};` | |
| }); | |
| const moon = ud.createElement('.moon', base, { | |
| title: `${m.max.raw.time} ${m.max.illum}%`, //@${moonPos(m)}`, | |
| style: `left:${toBasePct(m.max.time)}; bottom:${toBasePct(m.max.degr/90)};` | |
| }); | |
| if(m.wane) { moon.classList.add('wane'); } | |
| const pct = m.max.illum/100, | |
| small = (pct < .5), | |
| half = small ? (1 - 2*pct) : 2 * (pct-.5); | |
| moon.innerHTML = `<svg width="10" height="10" viewBox="0,0 2,2"> | |
| <circle cx="1" cy="1" r="1" fill="currentColor" /> | |
| <path d="M1,0 a1,1 0 1,0 0,2 a${half},1 0 1,${small ? 0 : 1} 0,-2" /> | |
| </svg>`; | |
| }); | |
| for(let d = 1; d <= e100.raw.date; d++) { | |
| const day = ud.createElement('.day', chart, { | |
| 'data-date': d, | |
| }), | |
| hours = ud.createElement('.hours', day); | |
| ['03', '06', '09', '12', '15', '18', '21'].forEach(h => ud.createElement('span', hours, { 'data-hour': h })); | |
| } | |
| return chart; | |
| } | |
| (function() { | |
| const ud = ABOUtils.DOM, | |
| [$, $$] = ud.selectors(); | |
| const form = $('form'), | |
| container = $('#container'), | |
| shortcuts = {}, | |
| loader = $('#loader'); | |
| $$('#shortcuts a').forEach(a => shortcuts[a.id] = a); | |
| //Stored (html) pages from timeanddate.com | |
| const htmlStore = new CompressedForage('moon-data'); | |
| function onError(err) { | |
| alert(err); | |
| toggleLoader(false); | |
| } | |
| function toggleLoader(show) { | |
| //loader.style.display = show ? 'flex' : 'none'; | |
| show ? loader.classList.add('show') : loader.classList.remove('show'); | |
| } | |
| function formSerialize(form) { | |
| //https://stackoverflow.com/a/44033425/1869660 | |
| const data = new FormData(form); | |
| return new URLSearchParams(data).toString(); | |
| } | |
| function formDeserialize(form, data) { | |
| const entries = (new URLSearchParams(data)).entries(); | |
| let entry; | |
| while(!(entry = entries.next()).done) { | |
| const [key, val] = entry.value, | |
| //http://javascript-coder.com/javascript-form/javascript-form-value.phtml | |
| input = form.elements[key]; | |
| switch(input.type) { | |
| case 'checkbox': input.checked = !!val; break; | |
| default: input.value = val; break; | |
| } | |
| } | |
| } | |
| function currDate() { | |
| const date = new Date(), | |
| year = date.getFullYear(), | |
| month = date.getMonth() + 1, | |
| day = date.getDate(); | |
| return { | |
| year, | |
| month, | |
| day, | |
| }; | |
| } | |
| function htmlKey(year, month, url) { | |
| month = ('0' + month).slice(-2); | |
| return `${year}-${month}-${url}`; | |
| } | |
| function init(callback) { | |
| //Form values (stored + default): | |
| const stored = localStorage.ABO_moonPhases; | |
| if(stored) { | |
| console.log('Stored:', stored); | |
| formDeserialize(form, stored); | |
| } | |
| const now = currDate(); | |
| form.elements.year.value = now.year; | |
| form.elements.month.value = now.month; | |
| function updateChart() { | |
| localStorage.ABO_moonPhases = formSerialize(form); | |
| doIt(); | |
| } | |
| ud.addEvent(form, 'submit', e => { | |
| e.preventDefault(); | |
| updateChart(); | |
| }); | |
| //For convenience, also update the chart when we click on a new month: | |
| ud.addEvent(form, 'change', e => { | |
| if(e.target.type === 'radio') { | |
| updateChart(); | |
| } | |
| }); | |
| //Scrolling the chart: | |
| ud.addEvent($('#shortcuts'), 'click', e => { | |
| e.preventDefault(); | |
| const chart = container.firstElementChild; | |
| switch(e.target.id) { | |
| case 'start': chart.scrollLeft = 0; break; | |
| case 'end': chart.scrollLeft = chart.scrollWidth; break; | |
| case 'now': chart.scrollLeft = $(`[data-date="${now.day}"]`, chart).offsetLeft; break; | |
| } | |
| }) | |
| //Delete stored pages that are more than a month old: | |
| htmlStore._onError = onError; | |
| function deleteHtml(keys, callback) { | |
| if(keys && keys.length) { | |
| const next = keys.shift(); | |
| console.log('Deleting ' + next); | |
| htmlStore.removeItem(next, () => deleteHtml(keys, callback)); | |
| } | |
| else { | |
| callback(); | |
| } | |
| } | |
| const prevMonthKey = htmlKey(now.year, now.month-1, ''), | |
| storedKeys = htmlStore.getKeys(keys => { | |
| const toDelete = keys.filter(k => k < prevMonthKey); | |
| deleteHtml(toDelete, callback); | |
| }); | |
| } | |
| function doIt() { | |
| toggleLoader(true); | |
| function getParam(key, toLower) { | |
| let val = form.elements[key].value; | |
| if(toLower) { val = val.toLowerCase(); } | |
| return val; | |
| } | |
| function parseHtml(html) { | |
| //console.log('html\n' + html); | |
| let table = html.substring(html.indexOf('<table id=tb-7dmn'), | |
| html.indexOf('</table>')) + '</table>'; | |
| //console.log('table\n' + table); | |
| const parser = ud.createElement('div'); | |
| parser.innerHTML = table; | |
| const tableElm = parser.firstElementChild; | |
| container.innerHTML = ''; | |
| container.appendChild(createTimeline(tableElm)); | |
| const nowLink = shortcuts.now; | |
| if(isNow) { | |
| nowLink.textContent = now.day; | |
| nowLink.style.display = 'inline-block'; | |
| nowLink.click(); | |
| } | |
| else { | |
| nowLink.style.display = 'none'; | |
| } | |
| toggleLoader(false); | |
| } | |
| //https://www.timeanddate.com/moon/norway/hamar?month=9&year=2018 | |
| const y = Number(getParam('year')), | |
| m = Number(getParam('month')), | |
| now = currDate(), | |
| isNow = (y === now.year) && (m === now.month); | |
| //We store the moon data (scraped html pages) locally, | |
| //as the moon's path and phases are unlikely to change: | |
| const url = `https://www.timeanddate.com/moon/${getParam('country', true)}/${getParam('city', true)}?month=${m}&year=${y}`, | |
| storeKey = htmlKey(y, m, url); | |
| htmlStore.getItem(storeKey, html => { | |
| if(html) { | |
| console.log('Found stored ' + url); | |
| parseHtml(html); | |
| } | |
| else { | |
| //*/ | |
| console.log('Fetching ' + url); | |
| fetch('https://cors-anywhere.herokuapp.com/' + url) | |
| .then(r => r.text()) | |
| .then(html => { | |
| htmlStore.setItem(storeKey, html, () => parseHtml(html)); | |
| }) | |
| .catch(onError); | |
| //*/ | |
| } | |
| }); | |
| } | |
| init(doIt); | |
| })(); |
| body { | |
| font-family: Georgia, sans-serif; | |
| header h2 { | |
| margin-bottom: 0; | |
| } | |
| button, input { | |
| font: inherit; | |
| box-sizing: border-box; | |
| } | |
| } | |
| #loader { | |
| position: absolute; | |
| top:0;left:0;bottom:0;right:0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(white, .5); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity .5s; | |
| &.show { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| &::before { | |
| content: 'Loading...'; | |
| display: inline-block; | |
| font-size: 1.5em; | |
| width: 7em; | |
| height: 7em; | |
| line-height: 6; | |
| text-align: center; | |
| border-radius: 100%; | |
| background: palegreen; | |
| //color: white; | |
| } | |
| } | |
| form { | |
| margin: 1em 0; | |
| > div { | |
| } | |
| label { | |
| cursor: pointer; | |
| white-space: nowrap; | |
| } | |
| .caption { | |
| padding-right: .5em; | |
| } | |
| .location, .month-picker { | |
| display: inline-block; | |
| } | |
| .location { | |
| //display: table; | |
| margin: 0 .5em .5em 0; | |
| label { | |
| display: table-row; | |
| > * { display: table-cell; } | |
| } | |
| input { | |
| width: 100%; | |
| min-width: 5em; | |
| } | |
| } | |
| @supports (display: grid) { | |
| & { | |
| display: grid; | |
| grid-template-areas: "loc month" | |
| ". month"; | |
| //grid-template-columns: minmax(1fr, 15em) auto; | |
| grid-template-rows: 1fr auto; | |
| grid-column-gap: 1em; | |
| justify-content: start; | |
| justify-items: stretch; | |
| } | |
| .location { | |
| grid-area: loc; | |
| margin: 0; | |
| } | |
| .month-picker { | |
| grid-area: month; | |
| } | |
| button[type="submit"] { | |
| width: 100%; | |
| } | |
| } | |
| } | |
| .month-picker { | |
| @mixin hide() { | |
| position: absolute; | |
| z-index: -1; | |
| opacity: 0; | |
| } | |
| display: inline-flex; | |
| flex-flow: column nowrap; | |
| //align-items: center; | |
| border: 1px solid gainsboro; | |
| .year { | |
| //background: gainsboro; | |
| padding: .2em .4em; | |
| input { | |
| width: 4em; | |
| } | |
| } | |
| .month { | |
| .caption { | |
| @include hide(); | |
| } | |
| .inputs { | |
| display: flex; | |
| flex-flow: row wrap; | |
| width: 9em; | |
| label { | |
| flex: 1 1 30%; | |
| margin: 1px; | |
| input { | |
| @include hide(); | |
| } | |
| span { | |
| display: inline-block; | |
| width: 100%; | |
| padding: .5em 0; | |
| text-align: center; | |
| background: aliceblue; | |
| border: 1px solid lightskyblue; | |
| box-sizing: border-box; | |
| } | |
| span:hover, | |
| input:checked + span { | |
| background: gold; | |
| border-color: orange; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #container { | |
| margin: 1em 0; | |
| } | |
| #shortcuts { | |
| a { | |
| display: inline-block; | |
| width: 2.5em; | |
| height: 2.5em; | |
| white-space: nowrap; | |
| text-indent: 100%; | |
| overflow: hidden; | |
| background: url("data:image/svg+xml,%3Csvg style='color:tomato;fill:black' xmlns='http://www.w3.org/2000/svg' width='1200' height='400' viewBox='0 0 555 185'%3E %3Cdefs%3E %3Cpath id='cal' d='M22 143h29v-29H22v29zm35 0h32v-29H57v29zm-35-36h29V75H22v32zm35 0h32V75H57v32zM22 69h29V40H22v29zm74 74h32v-29H96v29zM57 69h32V40H57v29zm77 74h29v-29h-29v29zm-38-36h32V75H96v32zM60 21V-8l-1-2-2-1h-6l-2 1-1 2v29l1 2 2 1h6l2-1 1-2zm74 86h29V75h-29v32zM96 69h32V40H96v29zm38 0h29V40h-29v29zm3-48V-8l-1-2-2-1h-7l-2 1-1 2v29l1 2 2 1h7l2-1 1-2zm38-6v128q1 5-3 9t-9 3H22q-5 0-9-3t-4-9V15q0-6 4-9t9-4h13V-8q0-6 4-11t12-5h6q7 0 11 5t5 11V2h39V-8q0-6 4-11t12-5h6q7 0 11 5t5 11V2h13q6 0 9 4t3 9z'/%3E %3C/defs%3E %3Csvg x='0' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='start' cx='36' cy='54' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3Csvg x='185' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='now' cx='112' cy='91' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3Csvg x='370' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='end' cx='149' cy='128' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3C/svg%3E") center/300% no-repeat; | |
| &#start { | |
| background-position: 0; | |
| } | |
| &#now { | |
| background-position: 50%; | |
| } | |
| &#end { | |
| background-position: 100%; | |
| } | |
| } | |
| } | |
| $day-width: 200px; | |
| $day-height: 100px; | |
| .moon-chart { | |
| position: relative; | |
| display: flex; | |
| align-items: flex-end; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| $altitude: royalblue; | |
| background: dodgerblue; | |
| background-image: | |
| repeating-linear-gradient(90deg, white 0, white 1px, transparent 0, transparent $day-width), | |
| repeating-linear-gradient(180deg, transparent 0, transparent $day-height*.33, $altitude 0, $altitude $day-height*.34); | |
| background-attachment: local; | |
| .day-base { | |
| position: absolute; | |
| bottom: 0; | |
| display: block; | |
| width: $day-width; | |
| height: $day-height; | |
| .moon-path { | |
| position: absolute; | |
| bottom: 0; | |
| path { | |
| fill: none; | |
| stroke: white; | |
| stroke-width: 2; | |
| stroke-dasharray: 6; | |
| } | |
| } | |
| .east-west-marker { | |
| position: absolute; | |
| bottom: 0; | |
| //52% instead of 50%: | |
| //Because of the moon's own orbit speed, it takes a little longer than 24hrs to circle around to the same point on the horizon: | |
| //https://cseligman.com/text/sky/moonmotion.htm | |
| width: 52%; | |
| box-sizing: border-box; | |
| border: 1px dashed yellow; | |
| border-top: none; | |
| transform: translateX(-50%); | |
| &::before, &::after { | |
| display: block; | |
| position: absolute; | |
| top: -1em; | |
| color: white; | |
| } | |
| &::before { | |
| content: 'E'; | |
| left: -.3em; | |
| } | |
| &::after { | |
| content: 'W'; | |
| right: -.4em; | |
| } | |
| } | |
| .moon { | |
| position: absolute; | |
| display: block; | |
| width: 1.5em; | |
| height: 1.5em; | |
| margin: -.75em; | |
| //background: rgba(white, .8); | |
| //border-radius: 100%; | |
| svg { | |
| width: 100%; | |
| height: 100%; | |
| color: white; | |
| fill: royalblue; | |
| } | |
| &.wane { | |
| transform: rotate(180deg); | |
| } | |
| } | |
| } | |
| .day { | |
| flex: 0 0 auto; | |
| position: relative; | |
| display: block; | |
| width: $day-width; | |
| height: $day-height; | |
| $dark: rgba(black, 0.6); | |
| background-image: | |
| //url("data:image/svg+xml,%3Csvg fill='none' stroke='white' stroke-width='.2' xmlns='http://www.w3.org/2000/svg' width='120' height='10' viewBox='0,0, 24,2'%3E %3Cpath d='M0,2v-2 M12,2v-2 M24,2v-2' /%3E %3Cpath d='M6,2v-1.5 M18,2v-1.5'/%3E %3Cpath d='M3,2v-1 M9,2v-1 M15,2v-1 M21,2v-1'/%3E %3C/svg%3E"), | |
| linear-gradient(90deg, $dark 0, $dark $day-width*.2, rgba(0,0,0,0) $day-width*.3, rgba(0,0,0,0) $day-width*.7, $dark $day-width*.8, $dark $day-width), | |
| linear-gradient(180deg, transparent $day-height*.95, green 0); | |
| //background-repeat: no-repeat; | |
| //background-size: 100%, auto, auto; | |
| //background-position: center 100%; | |
| pointer-events: none; | |
| box-sizing: border-box; | |
| border-right: 1px dashed white; | |
| &::after { | |
| content: attr(data-date); | |
| display: block; | |
| width: 1.5em; | |
| height: 1.5em; | |
| margin: auto; | |
| text-align: center; | |
| line-height: 1.5; | |
| background: #ff9; | |
| border-radius: 100%; | |
| } | |
| .hours { | |
| position: absolute; | |
| bottom:0; left:0; width: 100%; | |
| display: flex; | |
| justify-content: space-evenly; | |
| span { | |
| position: relative; | |
| display: inline-block; | |
| //width: 2px; | |
| height: 1.4em; | |
| font-family: sans-serif; | |
| font-size: .7em; | |
| color: white; | |
| &::before { | |
| content: ''; | |
| display: block; | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| transform: translateX(-50%); | |
| background: currentColor; | |
| } | |
| &::after { | |
| content: attr(data-hour); | |
| position: absolute; | |
| bottom:0; | |
| transform: translateX(-50%); | |
| } | |
| } | |
| } | |
| } | |
| } //.moon-chart |