chore: refactor /murmurations f14934a6
Steve Simkins · 2026-05-02 23:43 1 file(s) · +79 −126
src/pages/murmurations.astro +79 −126
1 1
---
2 +
import PageLayout from "@/layouts/Base.astro";
2 3
import Murmurations from "@/components/page/Murmurations.astro";
4 +
5 +
const meta = {
6 +
	title: "Murmurations",
7 +
	description: "Boids simulation — emergent flocking from local rules",
8 +
};
3 9
---
4 10
5 -
<html lang="en">
6 -
	<head>
7 -
		<meta charset="utf-8" />
8 -
		<meta name="viewport" content="width=device-width, initial-scale=1" />
9 -
		<meta name="apple-mobile-web-app-capable" content="yes" />
10 -
		<meta name="theme-color" content="#121113" />
11 -
		<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
12 -
		<title>Murmurations</title>
13 -
		<style>
14 -
			* { margin: 0; padding: 0; box-sizing: border-box; }
15 -
			html, body {
16 -
				width: 100%;
17 -
				height: 100vh;
18 -
				height: 100dvh;
19 -
				overflow: hidden;
20 -
				background: #121113;
21 -
				overscroll-behavior: none;
22 -
				touch-action: none;
23 -
				-webkit-tap-highlight-color: transparent;
24 -
			}
25 -
26 -
			.top-link {
27 -
				position: fixed;
28 -
				top: max(16px, env(safe-area-inset-top));
29 -
				color: #f5f3ee;
30 -
				font-size: 16px;
31 -
				z-index: 10;
32 -
				text-decoration: underline;
33 -
				font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
34 -
				padding: 8px;
35 -
				margin: -8px;
36 -
			}
37 -
			.top-link.title { left: max(16px, env(safe-area-inset-left)); }
38 -
			.top-link.back { right: max(16px, env(safe-area-inset-right)); }
39 -
40 -
			.info-btn {
41 -
				position: fixed;
42 -
				bottom: max(16px, env(safe-area-inset-bottom));
43 -
				right: max(16px, env(safe-area-inset-right));
44 -
				width: 40px;
45 -
				height: 40px;
46 -
				border-radius: 50%;
47 -
				border: 1px solid rgba(245, 243, 238, 0.6);
48 -
				background: rgba(18, 17, 19, 0.6);
49 -
				color: #f5f3ee;
50 -
				font-family: ui-serif, Georgia, serif;
51 -
				font-style: italic;
52 -
				font-size: 18px;
53 -
				cursor: pointer;
54 -
				z-index: 10;
55 -
				display: flex;
56 -
				align-items: center;
57 -
				justify-content: center;
58 -
				padding: 0;
59 -
				-webkit-tap-highlight-color: transparent;
60 -
			}
61 -
			.info-btn:hover, .info-btn:active { background: rgba(245, 243, 238, 0.15); }
62 -
63 -
			.info-panel {
64 -
				position: fixed;
65 -
				bottom: calc(max(16px, env(safe-area-inset-bottom)) + 52px);
66 -
				right: max(16px, env(safe-area-inset-right));
67 -
				left: max(16px, env(safe-area-inset-left));
68 -
				max-width: 340px;
69 -
				margin-left: auto;
70 -
				background: rgba(18, 17, 19, 0.94);
71 -
				border: 1px solid rgba(245, 243, 238, 0.2);
72 -
				color: #f5f3ee;
73 -
				padding: 16px 18px;
74 -
				font-size: 13px;
75 -
				line-height: 1.5;
76 -
				z-index: 10;
77 -
				display: none;
78 -
				font-family: ui-sans-serif, system-ui, sans-serif;
79 -
			}
80 -
			.info-panel.open { display: block; }
81 -
			.info-panel h2 {
82 -
				font-size: 14px;
83 -
				margin-bottom: 8px;
84 -
				font-weight: 600;
85 -
			}
86 -
			.info-panel p { margin-bottom: 8px; }
87 -
			.info-panel p:last-child { margin-bottom: 0; }
88 -
			.info-panel a { color: #f5f3ee; text-decoration: underline; }
89 -
90 -
			@media (max-width: 480px) {
91 -
				.top-link { font-size: 14px; }
92 -
				.info-panel { font-size: 12px; padding: 14px; }
93 -
			}
94 -
		</style>
95 -
	</head>
96 -
	<body>
97 -
		<span class="top-link title">Murmurations</span>
98 -
		<a href="/" class="top-link back">Back</a>
99 -
		<Murmurations />
100 -
		<button id="info-btn" class="info-btn" aria-label="About this simulation" aria-expanded="false">i</button>
101 -
		<div id="info-panel" class="info-panel" role="dialog" aria-label="About this simulation">
102 -
			<h2>Boids &amp; Starlings</h2>
103 -
			<p>
104 -
				A <em>murmuration</em> is the swirling, shape-shifting flight of thousands of starlings at dusk.
105 -
				No leader directs them — coherence emerges from each bird reacting to its closest neighbors.
106 -
			</p>
107 -
			<p>
108 -
				This simulation uses <a href="https://en.wikipedia.org/wiki/Boids" target="_blank" rel="noreferrer">Craig Reynolds&apos; boids algorithm</a>:
109 -
				each agent follows three rules — <strong>separation</strong> (avoid crowding),
110 -
				<strong>alignment</strong> (match neighbor heading), and <strong>cohesion</strong> (steer toward neighbor center).
111 -
			</p>
112 -
			<p>
113 -
				Real starlings track ~7 nearest neighbors regardless of distance — topological, not metric.
114 -
				That fixed count is why turning waves cross the flock so fast and stay crisp at any density.
115 -
			</p>
116 -
		</div>
117 -
		<script>
118 -
			const btn = document.getElementById('info-btn')!;
119 -
			const panel = document.getElementById('info-panel')!;
120 -
			btn.addEventListener('click', (e) => {
11 +
<PageLayout meta={meta}>
12 +
	<Murmurations />
13 +
	<style is:global>
14 +
		body.murmurations-page {
15 +
			padding: 0 !important;
16 +
			max-width: none !important;
17 +
		}
18 +
		body.murmurations-page #main-header,
19 +
		body.murmurations-page footer {
20 +
			display: none !important;
21 +
		}
22 +
		body.murmurations-page #main {
23 +
			flex: none;
24 +
		}
25 +
	</style>
26 +
	<script>
27 +
		document.body.classList.add("murmurations-page");
28 +
	</script>
29 +
	<a
30 +
		href="/"
31 +
		class="fixed bottom-4 left-4 z-10 text-sm underline"
32 +
		style="bottom: max(1rem, env(safe-area-inset-bottom)); left: max(1rem, env(safe-area-inset-left));"
33 +
	>
34 +
		Back
35 +
	</a>
36 +
	<button
37 +
		id="info-btn"
38 +
		class="fixed right-4 bottom-4 z-10 flex h-10 w-10 items-center justify-center rounded-full border border-textColor/60 bg-bgColor/60 p-0 text-base hover:bg-textColor/15 active:bg-textColor/15"
39 +
		style="bottom: max(1rem, env(safe-area-inset-bottom)); right: max(1rem, env(safe-area-inset-right));"
40 +
		aria-label="About this simulation"
41 +
		aria-expanded="false"
42 +
	>
43 +
		i
44 +
	</button>
45 +
	<div
46 +
		id="info-panel"
47 +
		class="fixed right-4 left-4 z-10 ml-auto hidden max-w-sm border border-textColor/20 bg-bgColor/95 p-4 text-xs leading-relaxed"
48 +
		style="bottom: calc(max(1rem, env(safe-area-inset-bottom)) + 3.25rem); right: max(1rem, env(safe-area-inset-right)); left: max(1rem, env(safe-area-inset-left));"
49 +
		role="dialog"
50 +
		aria-label="About this simulation"
51 +
	>
52 +
		<h2 class="mb-2 text-sm font-semibold">Boids &amp; Starlings</h2>
53 +
		<p class="mb-2">
54 +
			A <em>murmuration</em> is the swirling, shape-shifting flight of thousands of starlings at dusk.
55 +
      No leader directs them; coherence emerges from each bird reacting to its closest neighbors.
56 +
		</p>
57 +
		<p class="mb-2">
58 +
			This simulation uses <a class="underline" href="https://en.wikipedia.org/wiki/Boids" target="_blank" rel="noreferrer">Craig Reynolds&apos; boids algorithm</a>:
59 +
			each agent follows three rules — <strong>separation</strong> (avoid crowding),
60 +
			<strong>alignment</strong> (match neighbor heading), and <strong>cohesion</strong> (steer toward neighbor center).
61 +
		</p>
62 +
		<p>
63 +
			Real starlings track ~7 nearest neighbors regardless of distance — topological, not metric.
64 +
			That fixed count is why turning waves cross the flock so fast and stay crisp at any density.
65 +
		</p>
66 +
	</div>
67 +
	<script>
68 +
		const btn = document.getElementById("info-btn") as HTMLButtonElement | null;
69 +
		const panel = document.getElementById("info-panel") as HTMLDivElement | null;
70 +
		if (btn && panel) {
71 +
			btn.addEventListener("click", (e) => {
121 72
				e.stopPropagation();
122 -
				const open = panel.classList.toggle('open');
123 -
				btn.setAttribute('aria-expanded', String(open));
73 +
				const willOpen = panel.classList.contains("hidden");
74 +
				panel.classList.toggle("hidden", !willOpen);
75 +
				btn.setAttribute("aria-expanded", String(willOpen));
124 76
			});
125 -
			document.addEventListener('click', (e) => {
126 -
				if (!panel.classList.contains('open')) return;
127 -
				if (panel.contains(e.target as Node)) return;
128 -
				panel.classList.remove('open');
129 -
				btn.setAttribute('aria-expanded', 'false');
77 +
			document.addEventListener("click", (e) => {
78 +
				if (panel.classList.contains("hidden")) return;
79 +
				const target = e.target as Node;
80 +
				if (panel.contains(target) || btn.contains(target)) return;
81 +
				panel.classList.add("hidden");
82 +
				btn.setAttribute("aria-expanded", "false");
130 83
			});
131 -
		</script>
132 -
	</body>
133 -
</html>
84 +
		}
85 +
	</script>
86 +
</PageLayout>