Implement canvas-based bus system
Before Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 173 B |
@ -1,54 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<style>
|
<meta charset="utf-8" />
|
||||||
#road-container {
|
<title>Bus Progress</title>
|
||||||
width: 100%;
|
<link rel="stylesheet" href="drive.css" />
|
||||||
height: 100px;
|
<script src="drive.js"></script>
|
||||||
left: 0;
|
</head>
|
||||||
top: 0;
|
<body>
|
||||||
position: absolute;
|
<canvas id="road" width="1920" height="100"></canvas>
|
||||||
// margin: 980px 0 0 -100px; // uncomment to move to bottom of screen
|
</body>
|
||||||
z-index: 3;
|
</html>
|
||||||
}
|
|
||||||
|
|
||||||
#road-container div {
|
|
||||||
height: 100px;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
background-position: 0px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeofday-left {
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeofday-right {
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
#stops {
|
|
||||||
z-index: 6;
|
|
||||||
background-image: url(stops.png);
|
|
||||||
}
|
|
||||||
|
|
||||||
#bus {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
margin-left: 27px;
|
|
||||||
z-index: 8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="road-container">
|
|
||||||
<div id="bus"></div>
|
|
||||||
<div id="timeofday-left"></div>
|
|
||||||
<div id="timeofday-right"></div>
|
|
||||||
<div id="stops"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="drive.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
@ -1,122 +1,198 @@
|
|||||||
|
const COLORS = {
|
||||||
const PAGE_WIDTH = 1920;
|
day: {
|
||||||
const MINUTES_PER_PAGE = 60;
|
sky: "#41cee2",
|
||||||
const POINT_WIDTH = PAGE_WIDTH * 8 * 60 / MINUTES_PER_PAGE;
|
ground: "#e5931b",
|
||||||
const MILES_PER_PAGE = 45;
|
surface: "#b77616",
|
||||||
const BUS_POSITION_X = 93;
|
},
|
||||||
const BASE_ODO = 109.3;
|
dusk: {
|
||||||
const UPDATE_INTERVAL_MS = 5000
|
sky: "#db92be",
|
||||||
const WUBLOADER_URL = "";
|
ground: "#dd926a",
|
||||||
const SKY_URLS = {
|
surface: "#b17555",
|
||||||
day: "db_day.png",
|
},
|
||||||
dawn: "db_dawn.png",
|
night: {
|
||||||
dusk: "db_dusk.png",
|
sky: "#121336",
|
||||||
night: "db_night.png",
|
ground: "#30201a",
|
||||||
};
|
surface: "#261a15",
|
||||||
const BUS_URLS = {
|
},
|
||||||
day: "bus_day.png",
|
dawn: {
|
||||||
dawn: "bus_day.png",
|
sky: "#2b2f87",
|
||||||
dusk: "bus_day.png",
|
ground: "#724d41",
|
||||||
night: "bus_night.png",
|
surface: "#5b3e34",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function setSkyElements(left, right, timeToTransition) {
|
// The width from the left side of the bus image to the front of the bus
|
||||||
const leftElement = document.getElementById("timeofday-left");
|
const BUS_FRONT_OFFSET = 73;
|
||||||
const rightElement = document.getElementById("timeofday-right");
|
|
||||||
const busElement = document.getElementById("bus");
|
|
||||||
|
|
||||||
leftElement.style.backgroundImage = `url(${SKY_URLS[left]})`;
|
// Start time of each day phase
|
||||||
rightElement.style.backgroundImage = `url(${SKY_URLS[right]})`;
|
const DAY_START_MINUTES = 450;
|
||||||
|
const DUSK_START_MINUTES = 1140;
|
||||||
|
const NIGHT_START_MINUTES = 1200;
|
||||||
|
const DAWN_START_MINUTES = 400;
|
||||||
|
|
||||||
if (left === right) {
|
const BUS_STOP_OFFSET = 6;
|
||||||
leftElement.style.width = "100%";
|
const POINT_OFFSET = 17;
|
||||||
} else {
|
|
||||||
const transitionPercent = timeToTransition / MINUTES_PER_PAGE;
|
|
||||||
leftElement.style.width = `${transitionPercent * 100}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
bus.style.backgroundImage = `url(${BUS_URLS[left]})`;
|
// Bus stop positions are recorded in miles with the 0 position
|
||||||
}
|
// at route start. This array can be looped every point.
|
||||||
|
const BUS_STOP_POSITIONS = [1, 55.2, 125.4, 166.3, 233.9, 295.2];
|
||||||
|
|
||||||
function nextSkyTransition(timeofday, clock) {
|
// The default scaling factor is 20 seconds per pixel at max speed.
|
||||||
switch (timeofday) {
|
// This gives us
|
||||||
case "dawn":
|
// - 3px per minute
|
||||||
|
// - 4px per mile
|
||||||
|
let scaleFactor = 1;
|
||||||
|
|
||||||
|
function nextPhase(timeOfDay) {
|
||||||
|
switch (timeOfDay) {
|
||||||
case "day":
|
case "day":
|
||||||
return [19 * 60, "dusk"]; // 7pm
|
case "dawn":
|
||||||
|
return "dusk";
|
||||||
case "dusk":
|
case "dusk":
|
||||||
return [20 * 60, "night"]; // 8pm
|
return "night";
|
||||||
case "night":
|
case "night":
|
||||||
return [6 * 60 + 40, "dawn"]; // 6:40am
|
return "dawn";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSky(timeofday, clock) {
|
function phaseStartTime(timeOfDay) {
|
||||||
const [transition, newSky] = nextSkyTransition(timeofday, clock);
|
switch (timeOfDay) {
|
||||||
// 1440 minutes in 24h, this code will return time remaining even if
|
case "day":
|
||||||
// the transition is in the morning and we're currently in the evening.
|
return DAY_START_MINUTES;
|
||||||
const timeToTransition = (1440 + transition - clock) % 1440;
|
case "dusk":
|
||||||
if (timeToTransition < MINUTES_PER_PAGE) {
|
return DUSK_START_MINUTES;
|
||||||
// Transition on screen
|
case "night":
|
||||||
setSkyElements(timeofday, newSky, timeToTransition);
|
return NIGHT_START_MINUTES;
|
||||||
} else {
|
case "dawn":
|
||||||
// No transition on screen
|
return DAWN_START_MINUTES;
|
||||||
setSkyElements(timeofday, timeofday, undefined);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOdo(odo) {
|
function drawBackground(context, timeOfDay, leftX, width) {
|
||||||
const distancePixels = PAGE_WIDTH * (odo - BASE_ODO) / MILES_PER_PAGE;
|
const skyColor = COLORS[timeOfDay].sky;
|
||||||
const offset = (BUS_POSITION_X - distancePixels) % POINT_WIDTH;
|
const groundColor = COLORS[timeOfDay].ground;
|
||||||
|
const surfaceColor = COLORS[timeOfDay].surface;
|
||||||
|
|
||||||
const stopsElement = document.getElementById("stops");
|
context.fillStyle = COLORS[timeOfDay].sky;
|
||||||
stopsElement.style.backgroundPosition = `${offset}px 0px`;
|
context.fillRect(leftX, 0, width, 100);
|
||||||
|
context.fillStyle = COLORS[timeOfDay].surface;
|
||||||
|
context.fillRect(leftX, 80, width, 1);
|
||||||
|
context.fillStyle = COLORS[timeOfDay].ground;
|
||||||
|
context.fillRect(leftX, 81, width, 7);
|
||||||
|
context.fillRect(leftX, 89, width, 3);
|
||||||
|
context.fillRect(leftX, 94, width, 2);
|
||||||
|
context.fillRect(leftX, 99, width, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update() {
|
async function drawRoad() {
|
||||||
const busDataResponse = await fetch(`${WUBLOADER_URL}/thrimshim/bus/buscam`);
|
const busDataResponse = await fetch("/thrimshim/bus/buscam");
|
||||||
if (!busDataResponse.ok) {
|
if (!busDataResponse.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const busData = await busDataResponse.json();
|
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
|
const canvas = document.getElementById("road");
|
||||||
setSky("day", 7 * 60);
|
if (!canvas.getContext) {
|
||||||
setOdo(BASE_ODO);
|
return;
|
||||||
|
}
|
||||||
// Testing mode. Set true to enable.
|
const context = canvas.getContext("2d");
|
||||||
const test = false;
|
|
||||||
if (test) {
|
// Clear the previous canvas before starting
|
||||||
let h = 0;
|
context.clearRect(0, 0, 1920, 100);
|
||||||
// Set to how long 1h of in-game time should take in real time
|
|
||||||
const hourTimeMs = 1 * 1000;
|
const currentTime = busData.clock_minutes;
|
||||||
// Set to how often to update the screen
|
const distance = busData.odometer;
|
||||||
const interval = 30;
|
const timeOfDay = busData.timeofday;
|
||||||
setInterval(() => {
|
|
||||||
h += interval / hourTimeMs;
|
drawBackground(context, timeOfDay, 0, BUS_FRONT_OFFSET);
|
||||||
setOdo(BASE_ODO + 45 * h);
|
|
||||||
if (h < 19) {
|
const maxWidth = 1920 - BUS_FRONT_OFFSET;
|
||||||
setSky("day", 60 * h);
|
|
||||||
} else {
|
// TODO Figure out scaling factor
|
||||||
m = (h % 24) * 60;
|
const startMinute = busData.clock_minutes;
|
||||||
let tod;
|
const timeDuration = maxWidth / 3;
|
||||||
if (m < 6 * 60 + 40) {
|
|
||||||
tod = "night";
|
let previousTime = startMinute;
|
||||||
} else if (m < 19 * 60) {
|
let previousTimeOfDay = timeOfDay;
|
||||||
tod = "dawn";
|
let remainingDuration = timeDuration;
|
||||||
} else if (m < 20 * 60) {
|
let x = BUS_FRONT_OFFSET;
|
||||||
tod = "dusk";
|
while (remainingDuration > 0) {
|
||||||
|
const nextTimeOfDay = nextPhase(previousTimeOfDay);
|
||||||
|
const nextStartTime = phaseStartTime(nextTimeOfDay);
|
||||||
|
|
||||||
|
let thisDuration = nextStartTime - previousTime;
|
||||||
|
if (thisDuration < 0) {
|
||||||
|
thisDuration += 1440;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Figure out scaling factor
|
||||||
|
const pixelWidth = thisDuration * 3;
|
||||||
|
drawBackground(context, previousTimeOfDay, x, pixelWidth);
|
||||||
|
|
||||||
|
remainingDuration -= thisDuration;
|
||||||
|
previousTime = nextStartTime;
|
||||||
|
previousTimeOfDay = nextTimeOfDay;
|
||||||
|
x += pixelWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = BUS_FRONT_OFFSET;
|
||||||
|
const currentPointProgress = distance % 360;
|
||||||
|
let distanceToNextPoint;
|
||||||
|
if (currentPointProgress <= 109.3) {
|
||||||
|
distanceToNextPoint = 109.3 - currentPointProgress;
|
||||||
} else {
|
} else {
|
||||||
tod = "night";
|
distanceToNextPoint = 469.3 - currentPointProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Figure out scaling factor
|
||||||
|
x += distanceToNextPoint * 4;
|
||||||
|
const pointImage = new Image();
|
||||||
|
pointImage.src = "point.png";
|
||||||
|
context.drawImage(pointImage, x - POINT_OFFSET, 0);
|
||||||
|
while (x < maxWidth) {
|
||||||
|
// TODO Figure out scaling factor
|
||||||
|
x += 360 * 4;
|
||||||
|
context.drawImage(pointImage, x - POINT_OFFSET, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const distanceOnRoute = (distance - 109.3) % 360;
|
||||||
|
const busStopImage = new Image();
|
||||||
|
busStopImage.src = "db_stop.png";
|
||||||
|
// TODO Figure out scaling factor
|
||||||
|
let distanceTracked = distanceOnRoute - BUS_FRONT_OFFSET / 4;
|
||||||
|
if (distanceTracked < 0) {
|
||||||
|
distanceTracked += 720;
|
||||||
|
}
|
||||||
|
x = 0;
|
||||||
|
while (x < 1920) {
|
||||||
|
const distanceTrackedOnRoute = distanceTracked % 360;
|
||||||
|
let nextBusStopPosition = null;
|
||||||
|
for (const busStopPosition of BUS_STOP_POSITIONS) {
|
||||||
|
if (busStopPosition >= distanceTrackedOnRoute + 1) {
|
||||||
|
nextBusStopPosition = busStopPosition;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setSky(tod, m);
|
if (nextBusStopPosition === null) {
|
||||||
|
nextBusStopPosition = 360 + BUS_STOP_POSITIONS[0];
|
||||||
}
|
}
|
||||||
}, interval);
|
const nextBusStopDistance = nextBusStopPosition - distanceTrackedOnRoute;
|
||||||
} else {
|
distanceTracked += nextBusStopDistance;
|
||||||
// Do first update immediately, then every UPDATE_INTERVAL_MS
|
// TODO Figure out scaling factor
|
||||||
setInterval(update, UPDATE_INTERVAL_MS);
|
x += nextBusStopDistance * 4;
|
||||||
update();
|
context.drawImage(busStopImage, x - BUS_STOP_OFFSET, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const busImage = new Image();
|
||||||
|
if (timeOfDay === "night") {
|
||||||
|
busImage.src = "bus_night.png";
|
||||||
|
} else {
|
||||||
|
busImage.src = "bus_day.png";
|
||||||
|
}
|
||||||
|
context.drawImage(busImage, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
drawRoad();
|
||||||
|
setInterval(drawRoad, 10000);
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 397 B |
Before Width: | Height: | Size: 7.2 KiB |