Skip to content

Instantly share code, notes, and snippets.

@jbdamask
Created February 22, 2026 14:47
Show Gist options
  • Select an option

  • Save jbdamask/d3724a0208005cb985295da121ecef9c to your computer and use it in GitHub Desktop.

Select an option

Save jbdamask/d3724a0208005cb985295da121ecef9c to your computer and use it in GitHub Desktop.
NowIGetIt: temporarl-patterns.pdf
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Temporal Panel Selection in Citizens' Assemblies</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0a0f;
--bg2: #12121a;
--bg3: #1a1a2e;
--accent: #6c63ff;
--accent2: #ff6584;
--accent3: #43e97b;
--accent4: #f7971e;
--text: #e8e8f0;
--text2: #9999b0;
--glass: rgba(255,255,255,0.04);
--glass-border: rgba(255,255,255,0.08);
}
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
line-height: 1.7;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 3px; }
/* Canvas background */
#bg-canvas {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
pointer-events: none;
}
/* Navigation */
nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(20px);
background: rgba(10,10,15,0.7);
border-bottom: 1px solid var(--glass-border);
transition: transform 0.3s;
}
nav .logo {
font-weight: 800;
font-size: 1.1rem;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
nav .nav-links {
display: flex;
gap: 1.5rem;
list-style: none;
}
nav .nav-links a {
color: var(--text2);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
transition: color 0.3s;
position: relative;
}
nav .nav-links a:hover { color: var(--accent); }
nav .nav-links a::after {
content: '';
position: absolute;
bottom: -4px; left: 0;
width: 0; height: 2px;
background: var(--accent);
transition: width 0.3s;
}
nav .nav-links a:hover::after { width: 100%; }
/* Sections */
section {
position: relative;
z-index: 1;
padding: 6rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
/* Hero */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding-top: 5rem;
}
.hero .badge {
display: inline-block;
padding: 0.4rem 1.2rem;
border-radius: 50px;
background: rgba(108,99,255,0.15);
border: 1px solid rgba(108,99,255,0.3);
color: var(--accent);
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 2rem;
letter-spacing: 1px;
text-transform: uppercase;
}
.hero h1 {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #fff 0%, #6c63ff 50%, #ff6584 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
max-width: 900px;
}
.hero p {
font-size: 1.15rem;
color: var(--text2);
max-width: 650px;
margin-bottom: 3rem;
line-height: 1.8;
}
.hero .scroll-indicator {
margin-top: 2rem;
animation: bounce 2s infinite;
color: var(--accent);
font-size: 1.5rem;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(10px); }
}
/* Section headers */
.section-label {
display: inline-block;
padding: 0.3rem 1rem;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 1rem;
}
.section-title {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
margin-bottom: 1rem;
line-height: 1.2;
}
.section-desc {
font-size: 1.05rem;
color: var(--text2);
max-width: 700px;
margin-bottom: 3rem;
}
/* Cards */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.card {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 2rem;
transition: transform 0.3s, border-color 0.3s, box-shadow 0.3s;
position: relative;
overflow: hidden;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(108,99,255,0.3);
box-shadow: 0 20px 60px rgba(108,99,255,0.1);
}
.card .card-icon {
width: 48px; height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1.2rem;
}
.card h3 {
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 0.7rem;
}
.card p {
font-size: 0.9rem;
color: var(--text2);
line-height: 1.7;
}
/* Interactive Demo */
.demo-container {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 2.5rem;
margin-top: 2rem;
position: relative;
overflow: hidden;
}
.demo-container::before {
content: '';
position: absolute;
top: -50%; left: -50%;
width: 200%; height: 200%;
background: radial-gradient(circle at 30% 30%, rgba(108,99,255,0.05) 0%, transparent 50%);
pointer-events: none;
}
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
align-items: center;
}
.demo-btn {
padding: 0.6rem 1.4rem;
border-radius: 10px;
border: 1px solid var(--glass-border);
background: var(--glass);
color: var(--text);
font-family: inherit;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.demo-btn:hover, .demo-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
box-shadow: 0 4px 20px rgba(108,99,255,0.4);
}
.demo-btn.reset {
background: rgba(255,101,132,0.1);
border-color: rgba(255,101,132,0.3);
color: var(--accent2);
}
.demo-btn.reset:hover {
background: var(--accent2);
color: #fff;
}
.demo-canvas-wrap {
position: relative;
width: 100%;
height: 400px;
border-radius: 12px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--glass-border);
overflow: hidden;
}
#demo-canvas {
width: 100%;
height: 100%;
display: block;
}
.demo-legend {
display: flex;
gap: 1.5rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--text2);
}
.legend-dot {
width: 12px; height: 12px;
border-radius: 50%;
}
/* Timeline */
.timeline {
position: relative;
padding-left: 3rem;
margin-top: 2rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 2px;
background: linear-gradient(180deg, var(--accent), var(--accent2), var(--accent3));
}
.timeline-item {
position: relative;
margin-bottom: 3rem;
padding: 1.5rem 2rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 12px;
transition: border-color 0.3s;
}
.timeline-item:hover {
border-color: rgba(108,99,255,0.3);
}
.timeline-item::before {
content: '';
position: absolute;
left: -3rem;
top: 1.8rem;
width: 14px; height: 14px;
border-radius: 50%;
border: 3px solid var(--accent);
background: var(--bg);
transform: translateX(-6px);
}
.timeline-item .step-number {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: var(--accent);
font-weight: 600;
margin-bottom: 0.5rem;
}
.timeline-item h4 {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.timeline-item p {
font-size: 0.9rem;
color: var(--text2);
}
/* Comparison Table */
.comparison-table {
width: 100%;
border-collapse: collapse;
margin-top: 2rem;
font-size: 0.9rem;
}
.comparison-table th {
text-align: left;
padding: 1rem 1.2rem;
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text2);
border-bottom: 2px solid var(--glass-border);
}
.comparison-table td {
padding: 1rem 1.2rem;
border-bottom: 1px solid var(--glass-border);
color: var(--text2);
}
.comparison-table tr:hover td {
background: var(--glass);
}
.comparison-table .check {
color: var(--accent3);
font-weight: 700;
font-size: 1.1rem;
}
.comparison-table .cross {
color: var(--accent2);
font-weight: 700;
font-size: 1rem;
}
.comparison-table .approx {
font-family: 'JetBrains Mono', monospace;
color: var(--accent4);
font-weight: 600;
font-size: 0.85rem;
}
/* Toy example interactive */
.toy-example {
display: flex;
gap: 2rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.toy-population, .toy-panels {
flex: 1;
min-width: 300px;
}
.toy-population h4, .toy-panels h4 {
font-size: 0.9rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 1px;
}
.group-bar {
display: flex;
align-items: center;
margin-bottom: 0.8rem;
gap: 0.8rem;
}
.group-bar .label {
width: 80px;
font-size: 0.8rem;
font-weight: 600;
color: var(--text2);
}
.group-bar .bar-wrap {
flex: 1;
height: 28px;
background: rgba(255,255,255,0.05);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.group-bar .bar {
height: 100%;
border-radius: 6px;
transition: width 0.8s ease;
}
.group-bar .bar-text {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 0.7rem;
font-weight: 600;
color: rgba(255,255,255,0.7);
}
.panel-visual {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
align-items: center;
}
.panel-visual .panel-label {
font-size: 0.8rem;
font-weight: 700;
color: var(--text2);
width: 70px;
}
.seat {
width: 36px;
height: 36px;
border-radius: 8px;
border: 2px solid var(--glass-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
transition: all 0.5s;
position: relative;
}
.seat.filled {
border-color: transparent;
color: #fff;
}
/* Stats row */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.stat-card {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 1.5rem;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-card .stat-value {
font-size: 2.5rem;
font-weight: 900;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-card .stat-label {
font-size: 0.8rem;
color: var(--text2);
font-weight: 500;
}
/* Approach sections */
.approach-card {
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 2.5rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.approach-card .approach-number {
position: absolute;
top: -20px; right: 20px;
font-size: 8rem;
font-weight: 900;
color: rgba(108,99,255,0.05);
line-height: 1;
}
.approach-card .approach-tag {
display: inline-block;
padding: 0.3rem 0.8rem;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 700;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.approach-card h3 {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 0.8rem;
}
.approach-card p {
color: var(--text2);
font-size: 0.95rem;
margin-bottom: 1.2rem;
}
.guarantee-pills {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.pill {
padding: 0.4rem 0.9rem;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid;
}
/* Footer */
footer {
position: relative;
z-index: 1;
text-align: center;
padding: 4rem 2rem;
border-top: 1px solid var(--glass-border);
color: var(--text2);
font-size: 0.85rem;
}
footer a {
color: var(--accent);
text-decoration: none;
}
/* Animations */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* Responsive */
@media (max-width: 768px) {
nav .nav-links { display: none; }
section { padding: 4rem 1.2rem; }
.demo-canvas-wrap { height: 300px; }
.toy-example { flex-direction: column; }
}
/* Metric space visualization */
.metric-viz {
width: 100%;
height: 300px;
border-radius: 12px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--glass-border);
position: relative;
overflow: hidden;
margin-top: 1.5rem;
}
#metric-canvas {
width: 100%;
height: 100%;
}
/* Glow effects */
.glow-accent {
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.15;
pointer-events: none;
}
/* Code block style */
.formula-block {
background: rgba(0,0,0,0.4);
border: 1px solid var(--glass-border);
border-radius: 10px;
padding: 1.2rem 1.5rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--accent);
margin: 1rem 0;
overflow-x: auto;
}
.highlight {
color: var(--accent3);
font-weight: 600;
}
.highlight2 {
color: var(--accent2);
font-weight: 600;
}
.highlight3 {
color: var(--accent4);
font-weight: 600;
}
</style>
</head>
<body>
<canvas id="bg-canvas"></canvas>
<nav>
<div class="logo">Temporal Sortition</div>
<ul class="nav-links">
<li><a href="#problem">The Problem</a></li>
<li><a href="#how">How It Works</a></li>
<li><a href="#demo">Demo</a></li>
<li><a href="#approaches">Approaches</a></li>
<li><a href="#results">Results</a></li>
</ul>
</nav>
<!-- HERO -->
<section class="hero">
<div class="badge">📄 Research Paper Explained</div>
<h1>Making Democracy Fair Over Time</h1>
<p>How can rotating citizen panels represent <em>everyone</em>&mdash;even small groups&mdash;when no single panel is big enough? A new framework for temporal sortition makes it mathematically possible.</p>
<div style="display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center;">
<a href="#problem" class="demo-btn" style="text-decoration:none;">Learn More ↓</a>
<a href="#demo" class="demo-btn active" style="text-decoration:none;">Try the Demo ⚡</a>
</div>
<div class="scroll-indicator">↓</div>
</section>
<!-- PROBLEM -->
<section id="problem" class="fade-in">
<div class="section-label" style="background: rgba(108,99,255,0.15); color: var(--accent);">The Challenge</div>
<h2 class="section-title">What Are Citizens' Assemblies?</h2>
<p class="section-desc">Citizens' assemblies randomly select everyday people to deliberate on policy issues. Think of it as jury duty for democracy. But there's a fundamental tension between panel size and representation.</p>
<div class="card-grid">
<div class="card">
<div class="card-icon" style="background: rgba(108,99,255,0.15); color: var(--accent);">🎲</div>
<h3>Sortition</h3>
<p>People are randomly selected to form a panel, like ancient Athenian democracy. This avoids the biases of elections and ensures diverse perspectives.</p>
</div>
<div class="card">
<div class="card-icon" style="background: rgba(255,101,132,0.15); color: var(--accent2);">⚖️</div>
<h3>Representation</h3>
<p>Every sufficiently large group in the population should have someone "like them" on the panel. Groups are defined by similarity in a mathematical space.</p>
</div>
<div class="card">
<div class="card-icon" style="background: rgba(67,233,123,0.15); color: var(--accent3);">🔄</div>
<h3>Fairness Over Time</h3>
<p>Permanent assemblies rotate members periodically. This creates an opportunity: groups too small for one panel can be represented across multiple panels.</p>
</div>
</div>
<div class="demo-container" style="margin-top: 3rem;">
<h3 style="margin-bottom: 0.5rem;">The Representation Problem</h3>
<p style="color: var(--text2); font-size: 0.9rem; margin-bottom: 1.5rem;">Imagine a population with 4 groups of different sizes, and a panel with only 4 seats. The smallest groups can't both fit!</p>
<div class="toy-example" id="toy-example">
<div class="toy-population">
<h4>Population Distribution</h4>
<div class="group-bar">
<div class="label" style="color: #6c63ff;">Group A</div>
<div class="bar-wrap">
<div class="bar" style="width: 50%; background: linear-gradient(90deg, #6c63ff, #8b83ff);"></div>
<div class="bar-text">50%</div>
</div>
</div>
<div class="group-bar">
<div class="label" style="color: #ff6584;">Group B</div>
<div class="bar-wrap">
<div class="bar" style="width: 25%; background: linear-gradient(90deg, #ff6584, #ff8da3);"></div>
<div class="bar-text">25%</div>
</div>
</div>
<div class="group-bar">
<div class="label" style="color: #43e97b;">Group C</div>
<div class="bar-wrap">
<div class="bar" style="width: 12.5%; background: linear-gradient(90deg, #43e97b, #66f09b);"></div>
<div class="bar-text">12.5%</div>
</div>
</div>
<div class="group-bar">
<div class="label" style="color: #f7971e;">Group D</div>
<div class="bar-wrap">
<div class="bar" style="width: 12.5%; background: linear-gradient(90deg, #f7971e, #ffc247);"></div>
<div class="bar-text">12.5%</div>
</div>
</div>
</div>
<div class="toy-panels" id="toy-panels-container">
<h4>Panel Assignments (4 seats each)</h4>
<div id="toy-panels-content"></div>
<div style="margin-top: 1rem;">
<button class="demo-btn" id="toy-single-btn">Single Panel</button>
<button class="demo-btn" id="toy-temporal-btn">Temporal (2 Panels)</button>
</div>
<p id="toy-explanation" style="font-size: 0.8rem; color: var(--text2); margin-top: 1rem;"></p>
</div>
</div>
</div>
</section>
<!-- HOW IT WORKS -->
<section id="how" class="fade-in">
<div class="section-label" style="background: rgba(67,233,123,0.15); color: var(--accent3);">Framework</div>
<h2 class="section-title">How Temporal Sortition Works</h2>
<p class="section-desc">People are placed in a "metric space" where distance measures how differently two people think or live. The closer two people are, the better one can represent the other.</p>
<div class="metric-viz">
<canvas id="metric-canvas"></canvas>
</div>
<p style="font-size: 0.8rem; color: var(--text2); margin-top: 0.5rem; text-align: center;">
<em>Interactive: hover to see representation distances. Each cluster is a group of similar citizens.</em>
</p>
<div style="margin-top: 3rem;">
<h3 style="margin-bottom: 1.5rem;">Three Key Properties</h3>
<div class="timeline">
<div class="timeline-item">
<div class="step-number">PROPERTY 01</div>
<h4>Representation Per Panel</h4>
<p>Every sufficiently large group of citizens gets at least one representative in each individual panel. A group of size ≥ <span class="highlight">n/k</span> (where n = population, k = panel size) deserves at least 1 seat.</p>
</div>
<div class="timeline-item">
<div class="step-number">PROPERTY 02</div>
<h4>Representation Over Time (Prefix)</h4>
<p>After <em>t</em> panels, the combined set of all selected members represents every group of size ≥ <span class="highlight2">n / Σk<sub>i</sub></span>. Smaller groups accumulate representation across rounds.</p>
</div>
<div class="timeline-item">
<div class="step-number">PROPERTY 03</div>
<h4>Individual Fairness</h4>
<p>Every citizen has an <span class="highlight3">equal probability</span> of being selected across the entire process. No one is systematically favored or excluded from participation.</p>
</div>
</div>
</div>
<div class="formula-block" style="margin-top: 2rem;">
<span style="color: var(--text2);">// Proportional Fairness Condition</span><br>
For any group S with |S| ≥ n/k:<br>
&nbsp;&nbsp;∃ v ∈ S, p ∈ Panel : <span style="color: var(--accent3);">d(v, p)</span> ≤ α · <span style="color: var(--accent2);">min<sub>y</sub> max<sub>u∈S</sub> d(u, y)</span><br><br>
<span style="color: var(--text2);">// Translation: someone on the panel is "close" to someone in the group,</span><br>
<span style="color: var(--text2);">// relative to how spread out the group itself is.</span>
</div>
</section>
<!-- INTERACTIVE DEMO -->
<section id="demo" class="fade-in">
<div class="section-label" style="background: rgba(255,101,132,0.15); color: var(--accent2);">Interactive</div>
<h2 class="section-title">See Temporal Selection in Action</h2>
<p class="section-desc">Watch how the Greedy Capture algorithm selects panels over time. Citizens are dots in a 2D space; selected panel members are highlighted. Watch smaller groups get represented across rounds.</p>
<div class="demo-container">
<div class="demo-controls">
<button class="demo-btn active" id="btn-step">Step Forward ▶</button>
<button class="demo-btn" id="btn-auto">Auto Play ⏵⏵</button>
<button class="demo-btn reset" id="btn-reset">Reset ↺</button>
<span style="font-size: 0.8rem; color: var(--text2); margin-left: auto;" id="demo-status">Panel 1 / 3 &mdash; Seats: 0 / 4</span>
</div>
<div class="demo-canvas-wrap">
<canvas id="demo-canvas"></canvas>
</div>
<div class="demo-legend">
<div class="legend-item"><div class="legend-dot" style="background: #555;"></div> Unselected Citizen</div>
<div class="legend-item"><div class="legend-dot" style="background: #6c63ff;"></div> Panel 1</div>
<div class="legend-item"><div class="legend-dot" style="background: #ff6584;"></div> Panel 2</div>
<div class="legend-item"><div class="legend-dot" style="background: #43e97b;"></div> Panel 3</div>
<div class="legend-item"><div class="legend-dot" style="border: 2px solid #fff; box-sizing: border-box;"></div> Capture Radius</div>
</div>
</div>
</section>
<!-- THREE APPROACHES -->
<section id="approaches" class="fade-in">
<div class="section-label" style="background: rgba(247,151,30,0.15); color: var(--accent4);">Algorithms</div>
<h2 class="section-title">Three Approaches to Temporal Selection</h2>
<p class="section-desc">The paper proposes three algorithms with different trade-offs between the strength of guarantees and computational tractability.</p>
<div class="approach-card">
<div class="approach-number">1</div>
<div class="approach-tag" style="background: rgba(108,99,255,0.15); color: var(--accent);">Section 3 &mdash; Warm-up</div>
<h3>Panel + Global Representation</h3>
<p>The simplest approach: first select a large pool of ℓ·k people with good representation, then partition them into ℓ panels. Each panel and the overall pool are well-represented.</p>
<p><strong>Key insight:</strong> When a large panel's size is divisible by the smaller panel's size, the sub-panel inherits representation guarantees. But if it's <em>not</em> divisible&mdash;surprisingly&mdash;no finite guarantee is possible!</p>
<div class="guarantee-pills">
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Per-Panel O(1)-PRF</div>
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Global O(1)-PRF</div>
<div class="pill" style="color: var(--accent2); border-color: rgba(255,101,132,0.3); background: rgba(255,101,132,0.1);">✗ Prefix Representation</div>
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Individual Fairness</div>
</div>
</div>
<div class="approach-card">
<div class="approach-number">2</div>
<div class="approach-tag" style="background: rgba(255,101,132,0.15); color: var(--accent2);">Section 4 &mdash; Nested Groups</div>
<h3>Panel + Prefix Representation (Nested)</h3>
<p>A hierarchical approach: build a tree of nested groups at different scales. Larger groups (needing representation in fewer panels) nest inside smaller ones. Walk the tree to assign representatives to panels in order.</p>
<p><strong>Trade-off:</strong> Guarantees for both individual panels and all prefixes, but the approximation factor grows <span class="highlight2">exponentially</span> with the number of panels (O(4<sup>ℓ</sup>)).</p>
<div class="guarantee-pills">
<div class="pill" style="color: var(--accent4); border-color: rgba(247,151,30,0.3); background: rgba(247,151,30,0.1);">~ Per-Panel O(4<sup>ℓ</sup>)-PFC</div>
<div class="pill" style="color: var(--accent4); border-color: rgba(247,151,30,0.3); background: rgba(247,151,30,0.1);">~ Prefix O(4<sup>ℓ-t</sup>)-PFC</div>
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Individual Fairness</div>
</div>
</div>
<div class="approach-card">
<div class="approach-number">3</div>
<div class="approach-tag" style="background: rgba(67,233,123,0.15); color: var(--accent3);">Section 5 &mdash; Chain-Based</div>
<h3>Prefix Representation (Chains) ⭐</h3>
<p>The most elegant approach: instead of nesting groups, <strong>link</strong> them into chains across time levels. Groups at different scales are connected so that representing the endpoint of a chain automatically covers all groups along it.</p>
<p><strong>Key innovation:</strong> Anchors prevent error accumulation. When a chain's group shrinks to half the radius, a new anchor is set, bounding total error to a <span class="highlight">geometric series</span> that converges to a constant!</p>
<div class="guarantee-pills">
<div class="pill" style="color: var(--accent2); border-color: rgba(255,101,132,0.3); background: rgba(255,101,132,0.1);">✗ Per-Panel Guarantee</div>
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Prefix O(1)-PFC ⭐</div>
<div class="pill" style="color: var(--accent3); border-color: rgba(67,233,123,0.3); background: rgba(67,233,123,0.1);">✓ Individual Fairness</div>
</div>
</div>
</section>
<!-- RESULTS COMPARISON -->
<section id="results" class="fade-in">
<div class="section-label" style="background: rgba(108,99,255,0.15); color: var(--accent);">Results</div>
<h2 class="section-title">Comparing the Approaches</h2>
<p class="section-desc">Each algorithm makes different trade-offs. Here's a side-by-side summary.</p>
<div style="overflow-x: auto;">
<table class="comparison-table">
<thead>
<tr>
<th>Property</th>
<th>Approach 1<br><small>Panel + Global</small></th>
<th>Approach 2<br><small>Nested Groups</small></th>
<th>Approach 3<br><small>Chain-Based</small></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Per-Panel Representation</strong></td>
<td><span class="check">✓</span> <span class="approx">O(1)-PRF</span></td>
<td><span class="check">✓</span> <span class="approx">O(4<sup>ℓ</sup>)-PFC</span></td>
<td><span class="cross">✗</span></td>
</tr>
<tr>
<td><strong>Global Representation</strong></td>
<td><span class="check">✓</span> <span class="approx">O(1)-PRF</span></td>
<td><span class="check">✓</span> <span class="approx">O(1)-PFC</span></td>
<td><span class="check">✓</span> <span class="approx">O(1)-PFC</span></td>
</tr>
<tr>
<td><strong>Prefix Representation</strong></td>
<td><span class="cross">✗</span></td>
<td><span class="check">✓</span> <span class="approx">O(4<sup>ℓ-t</sup>)-PFC</span></td>
<td><span class="check">✓</span> <span class="approx">O(1)-PFC ⭐</span></td>
</tr>
<tr>
<td><strong>Individual Fairness</strong></td>
<td><span class="check">✓</span></td>
<td><span class="check">✓</span></td>
<td><span class="check">✓</span></td>
</tr>
<tr>
<td><strong>Approximation Quality</strong></td>
<td><span class="approx">Constant</span></td>
<td><span class="approx">Exponential in ℓ</span></td>
<td><span class="approx">Constant ⭐</span></td>
</tr>
<tr>
<td><strong>Panel Sizes</strong></td>
<td>Equal only</td>
<td>Equal only</td>
<td>Variable ✓</td>
</tr>
</tbody>
</table>
</div>
<div class="stats-row" style="margin-top: 3rem;">
<div class="stat-card">
<div class="stat-value">3</div>
<div class="stat-label">Algorithms Proposed</div>
</div>
<div class="stat-card">
<div class="stat-value">O(1)</div>
<div class="stat-label">Best Prefix Approximation</div>
</div>
<div class="stat-card">
<div class="stat-value">19</div>
<div class="stat-label">PFC Factor (Chain-Based)</div>
</div>
<div class="stat-card">
<div class="stat-value">3</div>
<div class="stat-label">Open Questions Remain</div>
</div>
</div>
</section>
<!-- REAL WORLD -->
<section class="fade-in">
<div class="section-label" style="background: rgba(67,233,123,0.15); color: var(--accent3);">Impact</div>
<h2 class="section-title">Real-World Applications</h2>
<p class="section-desc">Permanent citizens' assemblies already exist. This research provides the mathematical foundation to make them truly fair over time.</p>
<div class="card-grid">
<div class="card">
<div class="card-icon" style="background: rgba(108,99,255,0.15); color: var(--accent);">🇧🇪</div>
<h3>Ostbelgien, Belgium</h3>
<p>Since 2019, the German-speaking community has a permanent Citizens' Council with rotating panels. This is the first institutionalized permanent assembly.</p>
</div>
<div class="card">
<div class="card-icon" style="background: rgba(255,101,132,0.15); color: var(--accent2);">🇪🇺</div>
<h3>European Union</h3>
<p>The EU has experimented with European Citizens' Panels, bringing randomly selected citizens from across the continent to deliberate on policy.</p>
</div>
<div class="card">
<div class="card-icon" style="background: rgba(67,233,123,0.15); color: var(--accent3);">🇮🇪</div>
<h3>Ireland</h3>
<p>Ireland's Citizens' Assembly led to historic referendums on marriage equality and abortion. One-off panels showed the power of sortition.</p>
</div>
</div>
</section>
<!-- OPEN QUESTIONS -->
<section class="fade-in">
<div class="section-label" style="background: rgba(247,151,30,0.15); color: var(--accent4);">Open Questions</div>
<h2 class="section-title">What's Still Unsolved?</h2>
<p class="section-desc">The paper leaves three tantalizing open questions for future research.</p>
<div class="card-grid">
<div class="card" style="border-left: 3px solid var(--accent);">
<h3 style="font-family: 'JetBrains Mono'; font-size: 0.9rem; color: var(--accent); margin-bottom: 0.8rem;">Open Question #1</h3>
<p>Can we achieve O(1)-PRF for both individual panels <em>and</em> the global panel when panels have <strong>different sizes</strong>?</p>
</div>
<div class="card" style="border-left: 3px solid var(--accent2);">
<h3 style="font-family: 'JetBrains Mono'; font-size: 0.9rem; color: var(--accent2); margin-bottom: 0.8rem;">Open Question #2</h3>
<p>Can we get O(1)-PFC or O(1)-PRF for <strong>both</strong> individual panels and all prefix panels simultaneously?</p>
</div>
<div class="card" style="border-left: 3px solid var(--accent3);">
<h3 style="font-family: 'JetBrains Mono'; font-size: 0.9rem; color: var(--accent3); margin-bottom: 0.8rem;">Open Question #3</h3>
<p>Can we achieve O(1)-PRF (not just PFC) for every prefix? PRF gives <strong>proportionally many</strong> representatives, not just one.</p>
</div>
</div>
</section>
<footer>
<p style="margin-bottom: 0.5rem;">Based on <strong>"Temporal Panel Selection in Ongoing Citizens' Assemblies"</strong></p>
<p>Yusuf Hakan Kalaycı & Evi Micha — University of Southern California — <a href="https://arxiv.org/abs/2602.16194" target="_blank">arXiv:2602.16194</a></p>
</footer>
<script>
// =============================================
// BACKGROUND CANVAS - Particle network
// =============================================
(function() {
const canvas = document.getElementById('bg-canvas');
const ctx = canvas.getContext('2d');
let particles = [];
let w, h;
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
class Particle {
constructor() {
this.x = Math.random() * w;
this.y = Math.random() * h;
this.vx = (Math.random() - 0.5) * 0.3;
this.vy = (Math.random() - 0.5) * 0.3;
this.r = Math.random() * 1.5 + 0.5;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > w) this.vx *= -1;
if (this.y < 0 || this.y > h) this.vy *= -1;
}
}
for (let i = 0; i < 60; i++) particles.push(new Particle());
function drawBg() {
ctx.clearRect(0, 0, w, h);
particles.forEach(p => p.update());
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(108,99,255,0.15)';
ctx.fill();
for (let j = i + 1; j < particles.length; j++) {
const q = particles[j];
const dist = Math.hypot(p.x - q.x, p.y - q.y);
if (dist < 150) {
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(q.x, q.y);
ctx.strokeStyle = `rgba(108,99,255,${0.06 * (1 - dist / 150)})`;
ctx.stroke();
}
}
}
requestAnimationFrame(drawBg);
}
drawBg();
})();
// =============================================
// SCROLL ANIMATIONS
// =============================================
const fadeEls = document.querySelectorAll('.fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('visible');
});
}, { threshold: 0.1 });
fadeEls.forEach(el => observer.observe(el));
// =============================================
// TOY EXAMPLE
// =============================================
(function() {
const container = document.getElementById('toy-panels-content');
const singleBtn = document.getElementById('toy-single-btn');
const temporalBtn = document.getElementById('toy-temporal-btn');
const explanation = document.getElementById('toy-explanation');
const colors = { A: '#6c63ff', B: '#ff6584', C: '#43e97b', D: '#f7971e' };
function renderPanels(panels, mode) {
container.innerHTML = '';
panels.forEach((panel, i) => {
const row = document.createElement('div');
row.className = 'panel-visual';
row.innerHTML = `<div class="panel-label">Panel ${i + 1}</div>`;
panel.forEach(group => {
const seat = document.createElement('div');
seat.className = 'seat filled';
seat.style.background = colors[group];
seat.textContent = group;
row.appendChild(seat);
});
container.appendChild(row);
});
if (mode === 'single') {
explanation.innerHTML = '⚠️ With a single panel of 4 seats, one of the small groups (<span style="color:#43e97b">C</span> or <span style="color:#f7971e">D</span>) must be left out. There simply aren\'t enough seats.';
} else {
explanation.innerHTML = '✅ With <strong>two panels over time</strong>, we alternate the last seat: <span style="color:#43e97b">C</span> gets represented in Panel 1, and <span style="color:#f7971e">D</span> in Panel 2. <strong>Everyone is represented across the sequence!</strong>';
}
}
singleBtn.addEventListener('click', () => {
singleBtn.classList.add('active');
temporalBtn.classList.remove('active');
renderPanels([['A', 'A', 'B', 'C']], 'single');
});
temporalBtn.addEventListener('click', () => {
temporalBtn.classList.add('active');
singleBtn.classList.remove('active');
renderPanels([['A', 'A', 'B', 'C'], ['A', 'A', 'B', 'D']], 'temporal');
});
renderPanels([['A', 'A', 'B', 'C']], 'single');
singleBtn.classList.add('active');
})();
// =============================================
// METRIC SPACE VIZ
// =============================================
(function() {
const canvas = document.getElementById('metric-canvas');
const ctx = canvas.getContext('2d');
let w, h;
let mouseX = -1000, mouseY = -1000;
function resize() {
const rect = canvas.parentElement.getBoundingClientRect();
w = canvas.width = rect.width;
h = canvas.height = rect.height;
}
resize();
window.addEventListener('resize', resize);
// Generate clusters
const clusters = [
{ cx: 0.15, cy: 0.3, r: 0.06, color: '#6c63ff', label: 'Urban Youth', n: 15 },
{ cx: 0.35, cy: 0.6, r: 0.08, color: '#ff6584', label: 'Suburban Families', n: 20 },
{ cx: 0.6, cy: 0.25, r: 0.05, color: '#43e97b', label: 'Rural Workers', n: 8 },
{ cx: 0.75, cy: 0.65, r: 0.07, color: '#f7971e', label: 'Seniors', n: 14 },
{ cx: 0.9, cy: 0.35, r: 0.04, color: '#e84393', label: 'Students', n: 6 },
];
const points = [];
clusters.forEach(c => {
for (let i = 0; i < c.n; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * c.r;
points.push({
x: c.cx + Math.cos(angle) * dist,
y: c.cy + Math.sin(angle) * dist,
color: c.color,
label: c.label,
baseX: c.cx + Math.cos(angle) * dist,
baseY: c.cy + Math.sin(angle) * dist,
});
}
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / w;
mouseY = (e.clientY - rect.top) / h;
});
canvas.addEventListener('mouseleave', () => {
mouseX = -1000;
mouseY = -1000;
});
function draw() {
ctx.clearRect(0, 0, w, h);
// Draw connections to nearest hovered
let hovered = null;
let minDist = Infinity;
points.forEach(p => {
const dx = p.x - mouseX;
const dy = p.y - mouseY;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
hovered = p;
}
});
if (hovered && minDist < 0.1) {
// Draw radius circle
ctx.beginPath();
ctx.arc(hovered.x * w, hovered.y * h, 0.1 * w, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
// Draw connections
points.forEach(p => {
const dx = p.x - hovered.x;
const dy = p.y - hovered.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 0.15 && p !== hovered) {
ctx.beginPath();
ctx.moveTo(hovered.x * w, hovered.y * h);
ctx.lineTo(p.x * w, p.y * h);
const alpha = 0.5 * (1 - d / 0.15);
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
ctx.lineWidth = 1;
ctx.stroke();
}
});
// Label
ctx.fillStyle = '#fff';
ctx.font = '12px Inter';
ctx.fillText(hovered.label, hovered.x * w + 10, hovered.y * h - 10);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '10px JetBrains Mono';
ctx.fillText(`d = distance to panel rep`, hovered.x * w + 10, hovered.y * h + 5);
}
// Draw cluster labels
clusters.forEach(c => {
ctx.fillStyle = c.color + '40';
ctx.beginPath();
ctx.arc(c.cx * w, c.cy * h, c.r * w * 1.3, 0, Math.PI * 2);
ctx.fill();
});
// Draw points
points.forEach(p => {
const px = p.x * w;
const py = p.y * h;
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fillStyle = p.color;
if (hovered && p === hovered) {
ctx.fillStyle = '#fff';
ctx.arc(px, py, 6, 0, Math.PI * 2);
}
ctx.fill();
});
// Labels
clusters.forEach(c => {
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '11px Inter';
ctx.textAlign = 'center';
ctx.fillText(c.label, c.cx * w, (c.cy + c.r + 0.04) * h);
ctx.textAlign = 'left';
});
requestAnimationFrame(draw);
}
draw();
})();
// =============================================
// MAIN INTERACTIVE DEMO
// =============================================
(function() {
const canvas = document.getElementById('demo-canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('demo-status');
let w, h;
function resize() {
const rect = canvas.parentElement.getBoundingClientRect();
w = canvas.width = rect.width;
h = canvas.height = rect.height;
}
resize();
window.addEventListener('resize', () => { resize(); draw(); });
const NUM_PANELS = 3;
const PANEL_SIZE = 4;
const panelColors = ['#6c63ff', '#ff6584', '#43e97b'];
// Generate population with clusters
const pop = [];
const clusterDefs = [
{ cx: 0.2, cy: 0.3, r: 0.08, n: 12 },
{ cx: 0.5, cy: 0.7, r: 0.1, n: 18 },
{ cx: 0.8, cy: 0.25, r: 0.06, n: 8 },
{ cx: 0.35, cy: 0.5, r: 0.05, n: 5 },
{ cx: 0.7, cy: 0.6, r: 0.07, n: 10 },
{ cx: 0.15, cy: 0.75, r: 0.04, n: 4 },
];
clusterDefs.forEach(c => {
for (let i = 0; i < c.n; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * c.r;
pop.push({
x: c.cx + Math.cos(angle) * dist,
y: c.cy + Math.sin(angle) * dist,
panel: -1,
selected: false,
captureRadius: 0,
animProgress: 0,
});
}
});
let currentPanel = 0;
let seatsFilledInPanel = 0;
let selectedOrder = [];
let captureAnimations = [];
let autoInterval = null;
function dist(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
function greedyCapture() {
// Find unselected citizen that is farthest from all selected
// Simplified: find the unselected point that maximizes minimum distance to any selected
const unselected = pop.filter(p => !p.selected);
if (unselected.length === 0) return null;
const selected = pop.filter(p => p.selected);
if (selected.length === 0) {
// Pick the one closest to the densest cluster center
let best = unselected[0];
let bestScore = -Infinity;
unselected.forEach(p => {
let nearbyCount = 0;
unselected.forEach(q => {
if (dist(p, q) < 0.15) nearbyCount++;
});
if (nearbyCount > bestScore) {
bestScore = nearbyCount;
best = p;
}
});
return best;
}
// Greedy capture: find unselected point that covers the most unrepresented people
// Simplified version: pick the unselected with the most unselected neighbors within some radius
// that are also far from existing selected members
let best = null;
let bestScore = -Infinity;
unselected.forEach(candidate => {
let score = 0;
unselected.forEach(other => {
const dOther = dist(candidate, other);
if (dOther < 0.15) {
// How far is 'other' from nearest selected?
let minSelected = Infinity;
selected.forEach(s => {
minSelected = Math.min(minSelected, dist(other, s));
});
score += minSelected;
}
});
if (score > bestScore) {
bestScore = score;
best = candidate;
}
});
return best;
}
function step() {
if (currentPanel >= NUM_PANELS) return;
const chosen = greedyCapture();
if (!chosen) return;
chosen.selected = true;
chosen.panel = currentPanel;
chosen.animProgress = 0;
selectedOrder.push(chosen);
// Compute capture radius for visualization
const unselected = pop.filter(p => !p.selected);
let maxDist = 0;
unselected.forEach(p => {
const d = dist(chosen, p);
if (d < 0.15) maxDist = Math.max(maxDist, d);
});
chosen.captureRadius = Math.max(maxDist, 0.05);
captureAnimations.push({
x: chosen.x, y: chosen.y,
maxR: chosen.captureRadius,
progress: 0,
color: panelColors[currentPanel],
});
seatsFilledInPanel++;
if (seatsFilledInPanel >= PANEL_SIZE) {
currentPanel++;
seatsFilledInPanel = 0;
}
updateStatus();
}
function updateStatus() {
const panel = Math.min(currentPanel + 1, NUM_PANELS);
const seats = currentPanel >= NUM_PANELS ? PANEL_SIZE : seatsFilledInPanel;
statusEl.textContent = currentPanel >= NUM_PANELS
? `All ${NUM_PANELS} panels filled! (${NUM_PANELS * PANEL_SIZE} representatives selected)`
: `Panel ${panel} / ${NUM_PANELS} — Seats: ${seats} / ${PANEL_SIZE}`;
}
function reset() {
pop.forEach(p => {
p.selected = false;
p.panel = -1;
p.animProgress = 0;
p.captureRadius = 0;
});
currentPanel = 0;
seatsFilledInPanel = 0;
selectedOrder = [];
captureAnimations = [];
if (autoInterval) {
clearInterval(autoInterval);
autoInterval = null;
document.getElementById('btn-auto').classList.remove('active');
}
updateStatus();
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('btn-auto').addEventListener('click', function() {
if (autoInterval) {
clearInterval(autoInterval);
autoInterval = null;
this.classList.remove('active');
} else {
this.classList.add('active');
autoInterval = setInterval(() => {
if (currentPanel >= NUM_PANELS) {
clearInterval(autoInterval);
autoInterval = null;
document.getElementById('btn-auto').classList.remove('active');
return;
}
step();
}, 600);
}
});
function draw() {
ctx.clearRect(0, 0, w, h);
// Draw capture animations
captureAnimations.forEach(a => {
a.progress = Math.min(a.progress + 0.03, 1);
const r = a.maxR * a.progress * w * 0.8;
ctx.beginPath();
ctx.arc(a.x * w, a.y * h, r, 0, Math.PI * 2);
ctx.strokeStyle = a.color + Math.floor(40 * (1 - a.progress)).toString(16).padStart(2, '0');
ctx.lineWidth = 1;
ctx.stroke();
});
captureAnimations = captureAnimations.filter(a => a.progress < 1);
// Draw connections between selected and nearby unselected
pop.filter(p => p.selected).forEach(s => {
pop.filter(p => !p.selected).forEach(u => {
const d = dist(s, u);
if (d < 0.1) {
ctx.beginPath();
ctx.moveTo(s.x * w, s.y * h);
ctx.lineTo(u.x * w, u.y * h);
ctx.strokeStyle = `rgba(255,255,255,${0.06 * (1 - d / 0.1)})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
});
});
// Draw unselected
pop.filter(p => !p.selected).forEach(p => {
ctx.beginPath();
ctx.arc(p.x * w, p.y * h, 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(100,100,120,0.6)';
ctx.fill();
});
// Draw selected with animation
pop.filter(p => p.selected).forEach(p => {
p.animProgress = Math.min(p.animProgress + 0.05, 1);
const size = 3 + 5 * p.animProgress;
const color = panelColors[p.panel];
// Glow
ctx.beginPath();
ctx.arc(p.x * w, p.y * h, size + 4, 0, Math.PI * 2);
ctx.fillStyle = color + '30';
ctx.fill();
// Point
ctx.beginPath();
ctx.arc(p.x * w, p.y * h, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
// Border
ctx.beginPath();
ctx.arc(p.x * w, p.y * h, size, 0, Math.PI * 2);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
requestAnimationFrame(draw);
}
draw();
updateStatus();
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment