import { LitElement, ReactiveController, ReactiveControllerHost, css, html, render } from "lit"; import { property } from "lit/decorators/property.js"; const terranMonthIntervals = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; const pendorMonthIntervals = [ 0, 1, 25, 49, 73, 97, 121, 145, 146, 147, 171, 195, 211, 243, 267, 291, 292, ]; const pendorMonthNames = [ "Yestar", "Narrin", "Nenim", "Sulim", "Virta", "Lothess", "Narnya", "Attendes", "Loende", "Cerim", "Urim", "Yavar", "Narquel", "Hiss", "Ring", "Mettare", ]; const centaurMonthIntervals = [0, 1, 43, 85, 145, 146, 147, 189, 249, 291, 292]; const centaurMonthNames = [ "Yestr", "Tuil", "Layr", "Yaiv", "Attendes", "Loende", "Quel", "Rive", "Cair", "Mettare", ]; const pendorWeekdayNames = ["Seren", "Anar", "Noren", "Aldea", "Erwer", "Elenya"]; const prefix = (n: number) => `${n < 10 ? "0" : ""}${n.toFixed(0)}`; export class ClockController implements ReactiveController { host: ReactiveControllerHost; value = new Date(); timeout: number; // Node and the DOM do not agree on the type. Grrr. // eslint-disable-next-line @typescript-eslint/no-explicit-any private _timerID?: any; constructor(host: ReactiveControllerHost, timeout = 1000) { (this.host = host).addController(this); this.timeout = timeout; } hostConnected() { this._timerID = setInterval(() => { this.value = new Date(); this.host.requestUpdate(); }, this.timeout); } hostDisconnected() { clearInterval(this._timerID); this._timerID = undefined; } } const styles = css` *, *::before, *::after { all: unset; display: revert; box-sizing: border-box; } :host { padding-top: 0; letter-spacing: 1px; --default-font-size: calc(clamp(0.63rem, calc(0.5rem + 0.63vw), 0.9rem)); font-family: Bitwise, Audiowide, Tahoma, Arial, Helvetica, sans-serif; flex: 0 1 auto; text-align: left; } div#clock { padding: 0.175rem 0.375rem 0.175rem 0.375rem; background-color: var(--pendorclock-background-color, #000030); color: var(--pendorclock-color, #ffffff); font-size: var(--pendorclock-font-size, --default-font-size); line-height: var(--pendorclock-line-height, 1.35); font-weight: var(--pendorclock-font-weight, 700); min-width: 20ch; max-width: 35ch; text-align: center; } `; const fontStyle = css` @font-face { font-family: "Audiowide"; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/audiowide/v20/l7gdbjpo0cum0ckerWCdlg_O.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } `; export class PendorClock extends LitElement { static get styles() { return styles; } clock: ClockController; @property({ type: Boolean, attribute: "with-internal-font" }) useInternalFont = false; @property({ type: Boolean, attribute: "centaurs" }) useCentaurCalendar = false; constructor() { super(); this.clock = new ClockController(this, 250); } connectedCallback() { super.connectedCallback(); if (!this.useInternalFont) { return; } if (document.getElementById("pendor-font-block")) { return; } const head = document.head || document.getElementsByTagName("head")[0]; const style = html``; render(style, head); } pendorDate(days: number) { const nextMonth = pendorMonthIntervals.findIndex((i) => i >= days); if (nextMonth === undefined || pendorMonthIntervals[nextMonth - 1] === undefined) { return undefined; } const thisMonth = nextMonth - 1; // Holidays! Which have no Day-of-Week or Day-of-Month if ([0, 145, 146, 291].includes(days)) { return `${pendorMonthNames[thisMonth]}`; } const dayOfMonth = days - pendorMonthIntervals[thisMonth]; const dayOfWeek = (Math.ceil(days) - 1) % 6; return `${pendorWeekdayNames[dayOfWeek]}, ${ pendorMonthNames[thisMonth] } ${dayOfMonth.toFixed(0)}`; } centaurDate(days: number) { const nextMonth = centaurMonthIntervals.findIndex((i) => i >= days); if (nextMonth === undefined || centaurMonthIntervals[nextMonth - 1] === undefined) { return undefined; } const thisMonth = nextMonth - 1; // Holidays! Which have no Day-of-Week or Day-of-Month if ([0, 145, 146, 291].includes(days)) { return `${centaurMonthNames[thisMonth]}`; } const dayOfMonth = days - centaurMonthIntervals[thisMonth]; const dayOfWeek = (Math.ceil(days) - 1) % 6; return `${pendorWeekdayNames[dayOfWeek]}, ${ centaurMonthNames[thisMonth] } ${dayOfMonth.toFixed(0)}`; } tick(now: Date) { let hours = terranMonthIntervals[now.getMonth()] + now.getDate(); if (now.getMonth() > 2 && now.getFullYear() % 4 == 0) { hours++; } // DST Calculation, and wildly wrong, but WTF. It was prejudiced against where I live, // sorry. I just liked watching the clock turnover at midnight every four days, when // Terra's and Pendor's clocks had the same midnight. hours = hours * 24 + now.getHours() - 16; // Canonically, Pendor was seeded in 1884 CE. This is just a convenience to get the dates // closer. This sixteen has nothing to do with the one above. const year = now.getFullYear() + 16; const days = hours / 30; hours = hours % 30; let seconds = (now.getSeconds() + now.getMinutes() * 60) / 2.25; const minutes = seconds / 40; seconds = seconds % 40; const timePart = `${hours.toFixed(0)}:${prefix(minutes)}:${prefix(seconds)}`; const daysPart = this.useCentaurCalendar ? this.centaurDate(days) : this.pendorDate(days); return `${daysPart}, 00${year.toFixed(0)}, ${timePart}`; } render() { return html`
${this.tick(this.clock.value)}
`; } }