diff --git a/thrimbletrimmer/driveclock/bus_day.png b/thrimbletrimmer/driveclock/bus_day.png
new file mode 100644
index 0000000..c85bccd
Binary files /dev/null and b/thrimbletrimmer/driveclock/bus_day.png differ
diff --git a/thrimbletrimmer/driveclock/bus_night.png b/thrimbletrimmer/driveclock/bus_night.png
new file mode 100644
index 0000000..bf58c3f
Binary files /dev/null and b/thrimbletrimmer/driveclock/bus_night.png differ
diff --git a/thrimbletrimmer/driveclock/db_dawn.png b/thrimbletrimmer/driveclock/db_dawn.png
new file mode 100644
index 0000000..552dc35
Binary files /dev/null and b/thrimbletrimmer/driveclock/db_dawn.png differ
diff --git a/thrimbletrimmer/driveclock/db_day.png b/thrimbletrimmer/driveclock/db_day.png
new file mode 100644
index 0000000..5cf7d3a
Binary files /dev/null and b/thrimbletrimmer/driveclock/db_day.png differ
diff --git a/thrimbletrimmer/driveclock/db_dusk.png b/thrimbletrimmer/driveclock/db_dusk.png
new file mode 100644
index 0000000..b58fb45
Binary files /dev/null and b/thrimbletrimmer/driveclock/db_dusk.png differ
diff --git a/thrimbletrimmer/driveclock/db_night.png b/thrimbletrimmer/driveclock/db_night.png
new file mode 100644
index 0000000..c444a53
Binary files /dev/null and b/thrimbletrimmer/driveclock/db_night.png differ
diff --git a/thrimbletrimmer/driveclock/drive.html b/thrimbletrimmer/driveclock/drive.html
new file mode 100644
index 0000000..54eb7b0
--- /dev/null
+++ b/thrimbletrimmer/driveclock/drive.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thrimbletrimmer/driveclock/drive.js b/thrimbletrimmer/driveclock/drive.js
new file mode 100644
index 0000000..5db2829
--- /dev/null
+++ b/thrimbletrimmer/driveclock/drive.js
@@ -0,0 +1,120 @@
+
+const PAGE_WIDTH = 1920;
+const MINUTES_PER_PAGE = 60;
+const POINT_WIDTH = PAGE_WIDTH * 8 * 60 / MINUTES_PER_PAGE;
+const MILES_PER_PAGE = 45;
+const BUS_POSITION_X = 93;
+const BASE_ODO = 109.3;
+const UPDATE_INTERVAL_MS = 5000
+const WUBLOADER_URL = "";
+const SKY_URLS = {
+ day: "db_day.png",
+ dawn: "db_dawn.png",
+ dusk: "db_dusk.png",
+ night: "db_night.png",
+};
+const BUS_URLS = {
+ day: "bus_day.png",
+ dawn: "bus_day.png",
+ dusk: "bus_day.png",
+ night: "bus_night.png",
+};
+
+function setSkyElements(left, right, timeToTransition) {
+ const leftElement = document.getElementById("sky-left");
+ const rightElement = document.getElementById("sky-right");
+ const busElement = document.getElementById("bus");
+
+ leftElement.style.backgroundImage = `url(${SKY_URLS[left]})`;
+ rightElement.style.backgroundImage = `url(${SKY_URLS[right]})`;
+
+ if (left === right) {
+ leftElement.style.width = "100%";
+ } else {
+ const transitionPercent = timeToTransition / MINUTES_PER_PAGE;
+ leftElement.style.width = `${transitionPercent * 100}%`
+ }
+
+ bus.style.backgroundImage = `url(${BUS_URLS[left]})`;
+}
+
+function nextSkyTransition(timeofday, clock) {
+ switch (timeofday) {
+ case "dawn":
+ case "day":
+ return [19 * 60, "dusk"]; // 7pm
+ case "dusk":
+ return [20 * 60, "night"]; // 8pm
+ case "night":
+ return [6 * 60 + 40, "dawn"]; // 6:40am
+ }
+}
+
+function setSky(timeofday, clock) {
+ const [transition, newSky] = nextSkyTransition(timeofday, clock);
+ // 1440 minutes in 24h, this code will return time remaining even if
+ // the transition is in the morning and we're currently in the evening.
+ const timeToTransition = (1440 + transition - clock) % 1440;
+ if (timeToTransition < MINUTES_PER_PAGE) {
+ // Transition on screen
+ setSkyElements(timeofday, newSky, timeToTransition);
+ } else {
+ // No transition on screen
+ setSkyElements(timeofday, timeofday, undefined);
+ }
+}
+
+function setOdo(odo) {
+ const distancePixels = PAGE_WIDTH * (odo - BASE_ODO) / MILES_PER_PAGE;
+ const offset = (BUS_POSITION_X - distancePixels) % POINT_WIDTH;
+
+ const stopsElement = document.getElementById("stops");
+ stopsElement.style.backgroundPosition = `${offset}px 0px`;
+}
+
+async function update() {
+ const busDataResponse = await fetch(`${WUBLOADER_URL}/thrimshim/bus/buscam`);
+ if (!busDataResponse.ok) {
+ return;
+ }
+ const busData = await busDataResponse.json();
+ console.log("Got data:", busData);
+ setOdo(busData.odometer);
+ setSky(busData.timeofday, busData.clock_minutes);
+}
+
+// Initial conditions, before the first refresh finishes
+setSky("day", 7 * 60);
+setOdo(BASE_ODO);
+
+// Testing mode. Set true to enable.
+const test = false;
+if (test) {
+ let h = 0;
+ // Set to how long 1h of in-game time should take in real time
+ const hourTimeMs = 1 * 1000;
+ // Set to how often to update the screen
+ const interval = 30;
+ setInterval(() => {
+ h += interval / hourTimeMs;
+ setOdo(BASE_ODO + 45 * h);
+ if (h < 19) {
+ setSky("day", 60 * h);
+ } else {
+ m = (h % 24) * 60;
+ let tod;
+ if (m < 6 * 60 + 40) {
+ tod = "night";
+ } else if (m < 19 * 60) {
+ tod = "dawn";
+ } else if (m < 20 * 60) {
+ tod = "dusk";
+ } else {
+ tod = "night";
+ }
+ setSky(tod, m);
+ }
+ }, interval);
+} else {
+ setInterval(update, UPDATE_INTERVAL_MS);
+}
diff --git a/thrimbletrimmer/driveclock/stops.png b/thrimbletrimmer/driveclock/stops.png
new file mode 100644
index 0000000..552266e
Binary files /dev/null and b/thrimbletrimmer/driveclock/stops.png differ