pendorclock/src/PendorClock.ts

223 lines
6.6 KiB
TypeScript

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`<style id="pendor-font-block" type="text/css">
${fontStyle}
</style>`;
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`<div id="clock" part="clock">${this.tick(this.clock.value)}</div>`;
}
}