Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 194 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#ffffff" />
<meta name="theme-color" content="#fbfbfa" />
<meta
name="description"
content="A tiny train dispatcher. Click anywhere to send a train chugging across the screen."
Expand All @@ -22,6 +22,18 @@
--paper: #fbfbfa;
--panel: rgba(255, 255, 255, 0.72);
--accent: #111;
--steam: #c9c9c7;
}

:root[data-theme="night"] {
color-scheme: dark;
--ink: #f4f0df;
--muted: #c5c0ad;
--faint: rgba(244, 240, 223, 0.24);
--paper: #09111f;
--panel: rgba(16, 27, 45, 0.72);
--accent: #f5d86f;
--steam: rgba(214, 225, 235, 0.66);
}

* {
Expand All @@ -36,7 +48,9 @@
body {
margin: 0;
overflow: hidden;
background: var(--paper);
background:
radial-gradient(circle at 50% 62%, rgba(246, 231, 175, 0), rgba(246, 231, 175, 0)),
var(--paper);
color: var(--ink);
font-family:
ui-sans-serif,
Expand All @@ -48,6 +62,15 @@
Arial,
sans-serif;
font-feature-settings: "kern";
transition:
background 300ms ease,
color 300ms ease;
}

:root[data-theme="night"] body {
background:
radial-gradient(circle at 20% 14%, rgba(246, 231, 175, 0.16), transparent 18%),
linear-gradient(180deg, #080f1d 0%, #101a2b 56%, #192334 100%);
}

main {
Expand Down Expand Up @@ -95,6 +118,93 @@
animation: wiggle 600ms ease-in-out;
}

.night-details {
position: fixed;
inset: 0;
z-index: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 300ms ease;
}

:root[data-theme="night"] .night-details {
opacity: 1;
}

.moon {
position: absolute;
top: 9vh;
left: clamp(28px, 9vw, 116px);
width: clamp(42px, 7vw, 72px);
aspect-ratio: 1;
border-radius: 50%;
background: #f5df8a;
box-shadow: 0 0 34px rgba(245, 216, 111, 0.36);
}

.moon::after {
content: "";
position: absolute;
top: 5%;
right: -7%;
width: 82%;
aspect-ratio: 1;
border-radius: 50%;
background: #09111f;
}

.star {
position: absolute;
width: 3px;
height: 3px;
border-radius: 50%;
background: #f7efd0;
box-shadow: 0 0 10px rgba(247, 239, 208, 0.72);
}

.star:nth-child(2) {
top: 12vh;
left: 38vw;
}

.star:nth-child(3) {
top: 18vh;
left: 71vw;
width: 2px;
height: 2px;
}

.star:nth-child(4) {
top: 31vh;
left: 85vw;
}

.star:nth-child(5) {
top: 27vh;
left: 17vw;
width: 2px;
height: 2px;
}

.track-lights {
position: absolute;
right: 22px;
bottom: 16px;
display: flex;
gap: 14px;
align-items: flex-end;
}

.track-lights span {
display: block;
width: 6px;
height: 18px;
border-radius: 999px 999px 2px 2px;
background: linear-gradient(#f5d86f 0 32%, #56636d 32% 100%);
box-shadow: 0 0 16px rgba(245, 216, 111, 0.5);
}

.tagline {
max-width: 28ch;
color: var(--muted);
Expand Down Expand Up @@ -127,18 +237,23 @@
.steam {
position: fixed;
z-index: 0;
color: #c9c9c7;
color: var(--steam);
font-size: 18px;
pointer-events: none;
animation: puff 1400ms ease-out forwards;
will-change: transform, opacity;
}

.mute-toggle {
.controls {
position: fixed;
top: 16px;
right: 16px;
z-index: 10;
display: flex;
gap: 8px;
}

.icon-toggle {
display: inline-flex;
width: 38px;
height: 38px;
Expand All @@ -157,13 +272,18 @@
transform 150ms ease;
}

.mute-toggle:hover {
.icon-toggle:hover {
background: #fff;
border-color: #b8b8b8;
transform: translateY(-1px);
}

.mute-toggle:focus-visible {
:root[data-theme="night"] .icon-toggle:hover {
background: rgba(26, 43, 67, 0.96);
border-color: rgba(245, 216, 111, 0.42);
}

.icon-toggle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
Expand Down Expand Up @@ -266,22 +386,46 @@
</head>
<body>
<main id="dispatcher">
<div class="night-details" aria-hidden="true">
<span class="moon"></span>
<span class="star"></span>
<span class="star"></span>
<span class="star"></span>
<span class="star"></span>
<div class="track-lights">
<span></span>
<span></span>
<span></span>
</div>
</div>

<div class="hero" id="hero">
<div class="hero-inner">
<span class="train-emoji" role="img" aria-label="train">🚂</span>
<span class="tagline">Click anywhere to dispatch a train.</span>
</div>
</div>

<button
class="mute-toggle"
id="mute"
type="button"
aria-label="Mute"
aria-pressed="false"
>
🔊
</button>
<div class="controls">
<button
class="icon-toggle"
id="theme"
type="button"
aria-label="Use night mode"
aria-pressed="false"
>
🌙
</button>
<button
class="icon-toggle"
id="mute"
type="button"
aria-label="Mute"
aria-pressed="false"
>
🔊
</button>
</div>

<div class="counter" id="counter" aria-live="polite">0 trains dispatched</div>
</main>
Expand All @@ -297,8 +441,10 @@

const dispatcher = document.querySelector("#dispatcher");
const hero = document.querySelector("#hero");
const themeButton = document.querySelector("#theme");
const muteButton = document.querySelector("#mute");
const counter = document.querySelector("#counter");
const themeMeta = document.querySelector('meta[name="theme-color"]');
const choo = new Audio("./public/choo-choo.mp3");
choo.preload = "auto";

Expand All @@ -308,6 +454,9 @@
let lastLane = -1;
let lastDirection = "rtl";
let muted = localStorage.getItem("wtc:muted") === "1";
let theme =
localStorage.getItem("wtc:theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "night" : "day");

function pick(items) {
return items[Math.floor(Math.random() * items.length)];
Expand All @@ -320,6 +469,19 @@
localStorage.setItem("wtc:muted", muted ? "1" : "0");
}

function syncThemeButton() {
const isNight = theme === "night";
document.documentElement.dataset.theme = theme;
themeButton.textContent = isNight ? "☀️" : "🌙";
themeButton.setAttribute(
"aria-label",
isNight ? "Use day mode" : "Use night mode",
);
themeButton.setAttribute("aria-pressed", String(isNight));
themeMeta.setAttribute("content", isNight ? "#09111f" : "#fbfbfa");
localStorage.setItem("wtc:theme", theme);
}

function updateCounter() {
const noun = count === 1 ? "train" : "trains";
counter.textContent = `${count} ${noun} dispatched`;
Expand Down Expand Up @@ -404,16 +566,33 @@
syncMuteButton();
});

themeButton.addEventListener("click", (event) => {
event.stopPropagation();
theme = theme === "night" ? "day" : "night";
syncThemeButton();
});

window.addEventListener("keydown", (event) => {
const isButtonKey =
(event.code === "Space" || event.code === "Enter") &&
event.target instanceof HTMLElement &&
event.target.closest("button");

if (isButtonKey) return;

if (event.code === "Space" || event.code === "Enter") {
event.preventDefault();
dispatchTrain();
} else if (event.key === "m" || event.key === "M") {
muted = !muted;
syncMuteButton();
} else if (event.key === "n" || event.key === "N") {
theme = theme === "night" ? "day" : "night";
syncThemeButton();
}
});

syncThemeButton();
syncMuteButton();
updateCounter();
</script>
Expand Down