Skip to content

Commit 22092c3

Browse files
committed
Hero: decorative boids flock behind the headline
Lightweight Reynolds flocking sim (50 triangles, separation/alignment/cohesion plus a soft edge cushion) rendered in an SVG layered behind the hero copy. Honors prefers-reduced-motion; opacity tuned low so the flock reads as ambient movement, not a graph.
1 parent a126a26 commit 22092c3

2 files changed

Lines changed: 189 additions & 1 deletion

File tree

web/src/pages/index.astro

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
138138
<!-- HERO -->
139139
<section class="hero">
140140
<div class="gridbg" aria-hidden="true"></div>
141+
<svg class="hero-boids" id="hero-boids" aria-hidden="true" viewBox="0 0 1200 600" preserveAspectRatio="xMidYMid slice">
142+
<g class="hero-boids-flock"></g>
143+
</svg>
141144
<div class="wrap">
142145
<div class="hero-top">
143146
<div class="chip" id="exchange-chip" aria-live="polite">
@@ -512,7 +515,7 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
512515
<div class="chart-head">
513516
<div>
514517
<div class="num">Agents · weekly</div>
515-
<div class="title">The network, <span class="accent"><span data-live="agents-long">{liveAgents}</span> agents</span> in {weeksLabel} weeks</div>
518+
<div class="title">The network, <span class="accent"><span data-live="agents-long">{liveAgents}</span> agents</span> in <span data-live="weeks">{weeksLabel}</span> weeks</div>
516519
</div>
517520
<div class="legend">
518521
<span><span class="swatch sw-a"></span>Pilot agents</span>
@@ -803,6 +806,92 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
803806
setInterval(shootPacket, 260);
804807
})();
805808

809+
// Hero boids - decorative flock of triangle-shaped boids drifting behind the
810+
// headline. Reynolds separation/alignment/cohesion in a 1200x600 viewBox with
811+
// a soft edge cushion. Reduced-motion renders a static frame.
812+
(function(){
813+
var svg = document.getElementById('hero-boids');
814+
if (!svg) return;
815+
var flockG = svg.querySelector('.hero-boids-flock');
816+
var W = 1200, H = 600;
817+
var TRI_POINTS = '-5,-3.2 -5,3.2 8.5,0';
818+
819+
var seed = 7;
820+
function rand(){ seed = (seed*9301 + 49297) % 233280; return seed/233280; }
821+
822+
var N = 50;
823+
var boids = [];
824+
for (var i = 0; i < N; i++) {
825+
var ang0 = rand() * Math.PI * 2;
826+
var sp0 = 0.8 + rand() * 0.6;
827+
boids.push({
828+
x: rand() * W,
829+
y: rand() * H,
830+
vx: Math.cos(ang0) * sp0,
831+
vy: Math.sin(ang0) * sp0,
832+
scale: 0.7 + rand() * 0.8
833+
});
834+
}
835+
836+
var tris = boids.map(function(b){
837+
var t = document.createElementNS('http://www.w3.org/2000/svg','polygon');
838+
t.setAttribute('points', TRI_POINTS);
839+
flockG.appendChild(t);
840+
return t;
841+
});
842+
843+
function paint(){
844+
for (var i = 0; i < boids.length; i++) {
845+
var b = boids[i];
846+
var ang = Math.atan2(b.vy, b.vx) * 180 / Math.PI;
847+
tris[i].setAttribute('transform', 'translate('+b.x.toFixed(1)+' '+b.y.toFixed(1)+') rotate('+ang.toFixed(1)+') scale('+b.scale.toFixed(2)+')');
848+
}
849+
}
850+
paint();
851+
852+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
853+
854+
var SEP_R = 28, ALIGN_R = 70, COH_R = 110;
855+
var SEP_W = 0.10, ALIGN_W = 0.05, COH_W = 0.00025;
856+
var EDGE_MARGIN = 80, EDGE_W = 0.0028;
857+
var MAX_SPEED = 1.5, MIN_SPEED = 0.55;
858+
859+
function tick(){
860+
for (var i = 0; i < boids.length; i++) {
861+
var b = boids[i];
862+
var sx = 0, sy = 0;
863+
var ax = 0, ay = 0, an = 0;
864+
var cx = 0, cy = 0, cn = 0;
865+
for (var j = 0; j < boids.length; j++) {
866+
if (i === j) continue;
867+
var o = boids[j];
868+
var dx = o.x - b.x, dy = o.y - b.y;
869+
var d2 = dx * dx + dy * dy;
870+
if (d2 < 0.01) continue;
871+
if (d2 < SEP_R * SEP_R) { var d = Math.sqrt(d2); sx -= dx / d; sy -= dy / d; }
872+
if (d2 < ALIGN_R * ALIGN_R) { ax += o.vx; ay += o.vy; an++; }
873+
if (d2 < COH_R * COH_R) { cx += o.x; cy += o.y; cn++; }
874+
}
875+
var fx = sx * SEP_W, fy = sy * SEP_W;
876+
if (an > 0) { fx += (ax / an - b.vx) * ALIGN_W; fy += (ay / an - b.vy) * ALIGN_W; }
877+
if (cn > 0) { fx += (cx / cn - b.x) * COH_W; fy += (cy / cn - b.y) * COH_W; }
878+
if (b.x < EDGE_MARGIN) fx += (EDGE_MARGIN - b.x) * EDGE_W;
879+
else if (b.x > W - EDGE_MARGIN) fx -= (b.x - (W - EDGE_MARGIN)) * EDGE_W;
880+
if (b.y < EDGE_MARGIN) fy += (EDGE_MARGIN - b.y) * EDGE_W;
881+
else if (b.y > H - EDGE_MARGIN) fy -= (b.y - (H - EDGE_MARGIN)) * EDGE_W;
882+
883+
b.vx += fx; b.vy += fy;
884+
var sp = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
885+
if (sp > MAX_SPEED) { b.vx = b.vx / sp * MAX_SPEED; b.vy = b.vy / sp * MAX_SPEED; }
886+
else if (sp < MIN_SPEED && sp > 0) { b.vx = b.vx / sp * MIN_SPEED; b.vy = b.vy / sp * MIN_SPEED; }
887+
b.x += b.vx; b.y += b.vy;
888+
}
889+
paint();
890+
requestAnimationFrame(tick);
891+
}
892+
requestAnimationFrame(tick);
893+
})();
894+
806895
// Live exchange counter - today's exchange volume on the network.
807896
// Purely local simulation: derives today's baseline from wall-clock and
808897
// drifts it forward in-page. No server calls. Tick interval intentionally
@@ -833,6 +922,14 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
833922
// Live stats refresh - fetches polo on page load and every 60s so numbers
834923
// stay current between site deploys. Build-time values are the fallback.
835924
(function(){
925+
var LAUNCH_TS = new Date('2026-02-09T00:00:00Z').getTime() / 1000;
926+
var TICKS = 10, EXP_K = 4.2;
927+
var SVG_W = 1200, PAD_L = 60, PAD_R = 60, PAD_T = 40, PAD_B = 40;
928+
var SVG_H = 360;
929+
var PLOT_W = SVG_W - PAD_L - PAD_R;
930+
var PLOT_H = SVG_H - PAD_T - PAD_B;
931+
var WORDS = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen','twenty'];
932+
836933
function fmtAgentsLong(n){
837934
if (n >= 1_000_000) return '~' + (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
838935
if (n >= 10_000) return '~' + Math.round(n / 1000) + ',000';
@@ -844,6 +941,11 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
844941
if (n >= 1_000_000) return Math.round(n / 1_000_000) + 'M+';
845942
return String(n);
846943
}
944+
function fmtAxis(v){
945+
if (v >= 1_000_000) return (v / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
946+
if (v >= 1_000) return (v / 1000).toFixed(v >= 10_000 ? 0 : 1).replace(/\.0$/, '') + 'K';
947+
return String(Math.round(v));
948+
}
847949
function growth7d(daily){
848950
if (!Array.isArray(daily) || daily.length < 2) return null;
849951
var last = daily[daily.length - 1];
@@ -863,6 +965,76 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
863965
function setAll(sel, value){
864966
document.querySelectorAll(sel).forEach(function(el){ el.textContent = value; });
865967
}
968+
function rebuildChart(finalVal){
969+
if (!Number.isFinite(finalVal) || finalVal <= 0) return;
970+
var svg = document.querySelector('.chart-wrap svg');
971+
if (!svg) return;
972+
973+
var weeksSinceLaunch = Math.max(1, Math.round((Date.now() / 1000 - LAUNCH_TS) / (7 * 86400)));
974+
var expDenom = Math.exp(EXP_K) - 1;
975+
var pts = [];
976+
for (var i = 0; i < TICKS; i++) {
977+
var t = i / (TICKS - 1);
978+
var v = finalVal * (Math.exp(EXP_K * t) - 1) / expDenom;
979+
var label = i === TICKS - 1 ? 'NOW' : ('W' + (1 + Math.round(((weeksSinceLaunch - 1) * i) / (TICKS - 1))));
980+
pts.push({ label: label, value: Math.round(v) });
981+
}
982+
var chartMax = pts[pts.length - 1].value || 1;
983+
var xFor = function(i){ return PAD_L + (i / (pts.length - 1)) * PLOT_W; };
984+
var yFor = function(v){ return PAD_T + (1 - v / chartMax) * PLOT_H; };
985+
var coords = pts.map(function(p, i){ return { x: xFor(i), y: yFor(p.value), label: p.label, value: p.value }; });
986+
987+
var SAMPLES = 200;
988+
var linePath = '';
989+
for (var j = 0; j < SAMPLES; j++) {
990+
var tt = j / (SAMPLES - 1);
991+
var xx = PAD_L + tt * PLOT_W;
992+
var vv = finalVal * (Math.exp(EXP_K * tt) - 1) / expDenom;
993+
var yy = yFor(vv);
994+
linePath += (j === 0 ? 'M' : ' L') + xx.toFixed(1) + ' ' + yy.toFixed(1);
995+
}
996+
var areaPath = linePath + ' L ' + (PAD_L + PLOT_W).toFixed(1) + ' ' + (PAD_T + PLOT_H).toFixed(1) + ' L ' + PAD_L.toFixed(1) + ' ' + (PAD_T + PLOT_H).toFixed(1) + ' Z';
997+
998+
var yTicks = [1.0, 0.5, 0.2, 0.05].map(function(frac){
999+
var v = chartMax * frac;
1000+
return { label: fmtAxis(v), y: yFor(v) };
1001+
});
1002+
1003+
var grid = svg.querySelector('.chart-grid');
1004+
if (grid) grid.innerHTML = yTicks.map(function(t){ return '<line x1="0" y1="'+t.y+'" x2="1200" y2="'+t.y+'"></line>'; }).join('');
1005+
1006+
var area = svg.querySelector('.chart-area');
1007+
if (area) area.setAttribute('d', areaPath);
1008+
var line = svg.querySelector('.chart-line');
1009+
if (line) line.setAttribute('d', linePath);
1010+
1011+
var dots = svg.querySelector('.chart-dots');
1012+
if (dots) dots.innerHTML = coords.map(function(c, i){
1013+
var isLast = i === coords.length - 1;
1014+
return '<circle cx="'+c.x+'" cy="'+c.y+'" r="'+(isLast ? 4 : 3)+'"' + (isLast ? ' class="chart-dot-now"' : '') + '></circle>';
1015+
}).join('');
1016+
1017+
var labels = svg.querySelector('.chart-labels');
1018+
if (labels) labels.innerHTML = coords.map(function(c, i){
1019+
var anchor = i === 0 ? 'start' : (i === coords.length - 1 ? 'end' : 'middle');
1020+
return '<text x="'+c.x+'" y="335" text-anchor="'+anchor+'">'+c.label+'</text>';
1021+
}).join('');
1022+
1023+
var yaxis = svg.querySelector('.chart-yaxis');
1024+
if (yaxis) yaxis.innerHTML = yTicks.map(function(t){
1025+
return '<text x="12" y="'+(t.y + 4)+'" text-anchor="start">'+t.label+'</text>';
1026+
}).join('');
1027+
1028+
var finalDot = coords[coords.length - 1];
1029+
var finalLabel = svg.querySelector('[data-live-chart-label]');
1030+
if (finalLabel) {
1031+
finalLabel.setAttribute('x', String(finalDot.x - 10));
1032+
finalLabel.setAttribute('y', String(finalDot.y - 14));
1033+
}
1034+
1035+
var weeksWord = WORDS[weeksSinceLaunch] || String(weeksSinceLaunch);
1036+
setAll('[data-live="weeks"]', weeksWord);
1037+
}
8661038
function refresh(){
8671039
fetch('https://polo.pilotprotocol.network/api/stats', { cache: 'no-store' })
8681040
.then(function(r){ return r.ok ? r.json() : null; })
@@ -877,6 +1049,7 @@ const yTicks = [1.0, 0.5, 0.2, 0.05].map(frac => {
8771049
document.querySelectorAll('[data-live-chart-label]').forEach(function(el){
8781050
el.textContent = longStr + ' agents';
8791051
});
1052+
rebuildChart(activeN);
8801053
}
8811054
if (typeof s.total_requests === 'number') {
8821055
setAll('[data-live="requests"]', fmtRequests(s.total_requests));

web/src/styles/system.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ h1.display,
155155
-webkit-mask-image: radial-gradient(ellipse at 50% 30%, black, transparent 70%);
156156
}
157157

158+
.hero-boids {
159+
position: absolute;
160+
inset: 0;
161+
width: 100%;
162+
height: 100%;
163+
pointer-events: none;
164+
color: var(--accent);
165+
opacity: 0.14;
166+
mask-image: radial-gradient(ellipse at 50% 35%, black 25%, transparent 80%);
167+
-webkit-mask-image: radial-gradient(ellipse at 50% 35%, black 25%, transparent 80%);
168+
}
169+
[data-theme="light"] .hero-boids { opacity: 0.09; }
170+
.hero-boids-flock polygon { fill: currentColor; }
171+
.hero .wrap { position: relative; z-index: 1; }
172+
158173
/* ============================================================
159174
THESIS
160175
============================================================ */

0 commit comments

Comments
 (0)