<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IXA - A Rust framework for building modular agent-based models</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Lilex:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0e0e10;
--surface: #18181b;
--border: #27272a;
--text: #fafafa;
--text-muted: #a1a1aa;
--accent: #d4d4d8;
--font-size-xs: 0.7rem;
--font-size-sm: 0.8rem;
--font-size-base: 1rem;
--font-size-display: 4rem;
font-size: 18px;
}
body {
font-family: "Lilex", monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
width: 100%;
max-width: 640px;
gap: 3rem;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.logo {
width: 120px;
height: auto;
opacity: 0.85;
transition: opacity 0.3s ease;
}
.logo:hover {
opacity: 1;
}
h1 {
font-size: var(--font-size-display);
font-weight: 700;
letter-spacing: 0.15em;
line-height: 1;
}
.tagline {
font-size: var(--font-size-base);
font-weight: 400;
color: var(--text-muted);
text-align: center;
line-height: 1.6;
max-width: 440px;
}
.get-started {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
width: 100%;
}
.get-started-label {
font-size: var(--font-size-sm);
font-weight: 500;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-muted);
}
.install-box {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.875rem 1.25rem;
font-family: "Lilex", monospace;
color: var(--text);
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
position: relative;
user-select: all;
}
.install-box:hover {
border-color: #3f3f46;
background: #1c1c1f;
}
.install-box .prompt {
color: var(--text-muted);
user-select: none;
}
.install-box .copy-hint {
font-size: var(--font-size-xs);
color: #52525b;
user-select: none;
margin-left: 0.5rem;
transition: color 0.2s ease;
}
.install-box:hover .copy-hint {
color: var(--text-muted);
}
.install-box.copied .copy-hint {
color: var(--accent);
}
.links {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: center;
}
.links a {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--text-muted);
text-decoration: none;
letter-spacing: 0.04em;
padding: 0.5rem 0;
border-bottom: 1px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.links a:hover {
color: var(--text);
border-bottom-color: var(--border);
}
.crab-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
main {
position: relative;
z-index: 1;
}
.crab {
position: absolute;
left: 0;
top: 0;
width: 48px;
height: 48px;
will-change: transform, filter;
}
</style>
</head>
<body>
<div class="crab-layer" id="crabLayer"></div>
<main>
<div class="hero">
<img src="ixa_logo.svg" alt="Ixa crab logo" class="logo" />
<h1>ixa</h1>
<p class="tagline">
A Rust framework for building modular agent-based models.
</p>
</div>
<div class="get-started">
<span class="get-started-label">Get Started</span>
<div class="install-box" onclick="copyInstall(this)" role="button" tabindex="0">
<span class="prompt">$</span>
<span class="command">cargo add ixa</span>
<span class="copy-hint">click to copy</span>
</div>
</div>
<nav class="links">
<a href="book/">Documentation</a>
<a href="doc/ixa/">API Reference</a>
<a href="https://github.com/CDCgov/ixa">GitHub</a>
</nav>
</main>
<script>
function copyInstall(el) {
navigator.clipboard.writeText("cargo add ixa");
el.classList.add("copied");
el.querySelector(".copy-hint").textContent = "copied!";
setTimeout(() => {
el.classList.remove("copied");
el.querySelector(".copy-hint").textContent = "click to copy";
}, 2000);
}
const layer = document.getElementById("crabLayer");
const crabs = [];
const INITIAL_COUNT = 5;
const CRAB_SIZE = 48;
const CRAB_RADIUS = CRAB_SIZE / 2;
const MAX_SPEED = 1.4;
const WANDER_STRENGTH = 0.03;
const SPAWN_SPEED_RANGE = 0.6;
const WANDER_JITTER = 0.6;
const BOUNCE_DAMPEN = 0.6;
const SUSCEPTIBLE = 1;
const INFECTED = 0;
let mouseX = -100, mouseY = -100;
const cursorImg = document.createElement("img");
cursorImg.src = "ixa_logo.svg";
cursorImg.className = "crab";
cursorImg.style.filter = `grayscale(${SUSCEPTIBLE})`;
cursorImg.style.opacity = "0.5";
cursorImg.style.transform = `translate3d(${mouseX}px,${mouseY}px,0)`;
layer.appendChild(cursorImg);
const cursorCrab = { el: cursorImg, x: mouseX, y: mouseY, vx: 0, vy: 0, gray: SUSCEPTIBLE, isCursor: true };
crabs.push(cursorCrab);
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX - CRAB_RADIUS;
mouseY = e.clientY - CRAB_RADIUS;
});
requestAnimationFrame(tick);
function spawnCrab(cx, cy) {
const img = document.createElement("img");
img.src = "ixa_logo.svg";
img.className = "crab";
img.style.filter = `grayscale(${SUSCEPTIBLE})`;
const angle = Math.random() * Math.PI * 2;
const speed = 0.4 + Math.random() * SPAWN_SPEED_RANGE;
const crab = {
el: img,
x: cx - CRAB_RADIUS + (Math.random() - 0.5) * 40,
y: cy - CRAB_RADIUS + (Math.random() - 0.5) * 40,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
wanderAngle: angle,
gray: SUSCEPTIBLE,
};
const flip = crab.vx < 0 ? " scaleX(-1)" : "";
img.style.transform = `translate3d(${crab.x}px,${crab.y}px,0)${flip}`;
layer.appendChild(img);
crabs.push(crab);
}
function tick() {
const w = window.innerWidth;
const h = window.innerHeight;
for (const c of crabs) {
if (c.isCursor) {
const prevX = c.x;
c.x = mouseX;
c.y = mouseY;
c.vx = c.x - prevX;
const flip = c.vx < 0 ? " scaleX(-1)" : "";
c.el.style.transform = `translate3d(${c.x}px,${c.y}px,0)${flip}`;
c.el.style.filter = `grayscale(${c.gray})`;
continue;
}
c.wanderAngle += (Math.random() - 0.5) * WANDER_JITTER;
c.vx += Math.cos(c.wanderAngle) * WANDER_STRENGTH;
c.vy += Math.sin(c.wanderAngle) * WANDER_STRENGTH;
const speed = Math.sqrt(c.vx * c.vx + c.vy * c.vy);
if (speed > MAX_SPEED) {
c.vx = (c.vx / speed) * MAX_SPEED;
c.vy = (c.vy / speed) * MAX_SPEED;
}
c.x += c.vx;
c.y += c.vy;
if (c.x < 0) { c.x = 0; c.vx = Math.abs(c.vx) * BOUNCE_DAMPEN; c.wanderAngle = Math.random() * Math.PI - Math.PI / 2; }
if (c.y < 0) { c.y = 0; c.vy = Math.abs(c.vy) * BOUNCE_DAMPEN; c.wanderAngle = Math.random() * Math.PI; }
if (c.x + CRAB_SIZE > w) { c.x = w - CRAB_SIZE; c.vx = -Math.abs(c.vx) * BOUNCE_DAMPEN; c.wanderAngle = Math.PI + (Math.random() - 0.5) * Math.PI; }
if (c.y + CRAB_SIZE > h) { c.y = h - CRAB_SIZE; c.vy = -Math.abs(c.vy) * BOUNCE_DAMPEN; c.wanderAngle = -Math.random() * Math.PI; }
const flip = c.vx < 0 ? " scaleX(-1)" : "";
c.el.style.transform = `translate3d(${c.x}px,${c.y}px,0)${flip}`;
c.el.style.filter = `grayscale(${c.gray})`;
}
for (let i = 0; i < crabs.length; i++) {
for (let j = i + 1; j < crabs.length; j++) {
const a = crabs[i], b = crabs[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
if (dx * dx + dy * dy < CRAB_SIZE * CRAB_SIZE) {
a.gray = INFECTED;
b.gray = INFECTED;
}
}
}
requestAnimationFrame(tick);
}
let firstClick = true;
document.addEventListener("click", (e) => {
if (e.target.closest(".install-box") || e.target.closest(".links")) return;
if (firstClick) {
firstClick = false;
for (let i = 0; i < INITIAL_COUNT; i++) {
spawnCrab(Math.random() * window.innerWidth, Math.random() * window.innerHeight);
}
}
spawnCrab(e.clientX, e.clientY);
});
</script>
</body>
</html>