Skip to content

Commit 232dd98

Browse files
committed
more fixes
1 parent cddfa51 commit 232dd98

8 files changed

Lines changed: 320 additions & 10 deletions

File tree

src/components/Header.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ import Logo from './Logo.astro';
287287
transform: rotate(-45deg);
288288
}
289289

290-
@media (max-width: 78rem) {
290+
@media (max-width: 64rem) {
291291
.nav-toggle {
292292
display: inline-grid;
293293
}

src/components/Hero.astro

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
---
2-
import { hero, motto, miniGame } from '../data/site';
2+
import { hero, motto, miniGame, funding } from '../data/site';
33
import Logo from './Logo.astro';
44
5+
// The WebGL commons field reflects real state: the share of the launch goal
6+
// funded biases how many gold "patron" nodes/beams light into the < > core.
7+
const fundedPct = Math.round((funding.committed / funding.goal) * 100);
8+
59
// Precompute the headline as a trusted HTML string. Keep the H1 visible at
610
// first paint: this text is the LCP candidate, so it must not wait for JS reveal.
711
const lastLineIndex = hero.headlineLines.length - 1;
@@ -74,7 +78,8 @@ const mottoHalf = Array.from({ length: 4 })
7478
</div>
7579

7680
<aside class="hero__crest" data-reveal style="--reveal-delay:320ms">
77-
<div class="crest">
81+
<div class="crest" data-hero-field data-fund={fundedPct}>
82+
<canvas class="crest__field" aria-hidden="true"></canvas>
7883
<Logo size={160} decorative class="crest__mark" />
7984
</div>
8085
<pre
@@ -290,6 +295,35 @@ const mottoHalf = Array.from({ length: 4 })
290295
max-width: clamp(13rem, 21vw, 18rem);
291296
margin-inline: auto;
292297
}
298+
/* The lazy WebGL "digital commons" field (app.ts). Sits between the halo and
299+
the mark; bleeds a little past the crest box (its shader vignette fades the
300+
edges). Transparent until the field starts, so no-WebGL / reduced-motion /
301+
no-JS keep the clean static emblem. */
302+
.crest__field {
303+
position: absolute;
304+
top: -42%;
305+
left: -42%;
306+
width: 184%;
307+
height: 184%;
308+
z-index: 1;
309+
pointer-events: none;
310+
opacity: 0;
311+
transition: opacity 1.1s var(--ease-out);
312+
}
313+
.crest.is-live .crest__field {
314+
opacity: 1;
315+
}
316+
/* when the field is live it supplies the glow + motion — stop the halo breathe
317+
and quiet it so they don't double up into mush */
318+
.crest.is-live::before {
319+
animation: none;
320+
opacity: 0.4;
321+
}
322+
@media (prefers-reduced-motion: reduce) {
323+
.crest__field {
324+
display: none;
325+
}
326+
}
293327
:global(.crest__mark) {
294328
position: absolute;
295329
top: 50%;

src/components/Tiers.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const meterCells = (fill: number) =>
4949
class="plan__window"
5050
role="img"
5151
aria-label={`Service level: ${plan.window} — ${plan.windowNote}`}
52+
title={`${plan.window} — ${plan.windowNote}`}
5253
>
5354
<svg
5455
class="plan__clock"

src/data/site.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ export const miniGame = {
116116
voidSecondLabel: 'second',
117117
voidSecondsLabel: 'seconds',
118118
voidSecondsShortLabel: 'SEC',
119+
// The toy closes its own argument: survived-seconds vs. a funded year.
120+
gameOverPitch: 'A funded maintainer lasts all year — not sixty seconds.',
119121
blockPunchlines: [
120122
'NO REPRO?',
121123
'NEEDS TESTS',

src/scripts/app.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,49 @@ function initTabEgg() {
14301430
});
14311431
}
14321432

1433+
/* ---------- Hero "digital commons" WebGL spectacle (lazy, gated) ----------
1434+
The one deliberate spectacle. Enhancement only: never under reduced-motion or
1435+
automation, lazy-imported + started only while the hero is on screen, paused
1436+
off-screen. If WebGL or the chunk fails, the static < > mark + halo remain. */
1437+
function initHeroFieldSpectacle() {
1438+
if (reduceMotion) return;
1439+
if (navigator.webdriver) return;
1440+
const crest = document.querySelector<HTMLElement>('[data-hero-field]');
1441+
const canvas = crest?.querySelector<HTMLCanvasElement>('.crest__field');
1442+
if (!crest || !canvas) return;
1443+
let controller: { start: () => void; stop: () => void } | null = null;
1444+
let loading = false;
1445+
const io = new IntersectionObserver(
1446+
(entries) => {
1447+
for (const entry of entries) {
1448+
if (entry.isIntersecting) {
1449+
if (controller) {
1450+
controller.start();
1451+
crest.classList.add('is-live');
1452+
} else if (!loading) {
1453+
loading = true;
1454+
import('./hero-field')
1455+
.then((m) => {
1456+
controller = m.initHeroField(canvas);
1457+
if (controller) {
1458+
controller.start();
1459+
crest.classList.add('is-live');
1460+
}
1461+
})
1462+
.catch(() => {
1463+
/* WebGL unavailable or chunk failed → keep the static emblem */
1464+
});
1465+
}
1466+
} else if (controller) {
1467+
controller.stop();
1468+
}
1469+
}
1470+
},
1471+
{ threshold: 0.01 }
1472+
);
1473+
io.observe(crest);
1474+
}
1475+
14331476
function init() {
14341477
initIntro();
14351478
initReveal();
@@ -1450,6 +1493,7 @@ function init() {
14501493
initScrollSpy();
14511494
initTabEgg();
14521495
initConsoleEgg();
1496+
initHeroFieldSpectacle();
14531497
// Safety net: ensure the mascot reveals even if the intro overlay was absent.
14541498
window.setTimeout(() => document.documentElement.classList.add('mascot-ready'), 50);
14551499
}

src/scripts/game.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,14 @@ export function createMascotGame(): Game {
460460
const label = seconds === 1 ? miniGame.voidSecondLabel : miniGame.voidSecondsLabel;
461461
const { finale } = pickGameOverFinale();
462462
if (!finale)
463-
return `${miniGame.voidGameOverTitle}\n${miniGame.voidGameOverPrefix} ${seconds} ${label}.`;
464-
return `${miniGame.voidGameOverTitle} / ${finale.title}\n${miniGame.voidGameOverPrefix} ${seconds} ${label}.\n${finale.void}\n${finale.detail}`;
463+
return `${miniGame.voidGameOverTitle}\n${miniGame.voidGameOverPrefix} ${seconds} ${label}.\n${miniGame.gameOverPitch}`;
464+
return `${miniGame.voidGameOverTitle} / ${finale.title}\n${miniGame.voidGameOverPrefix} ${seconds} ${label}.\n${finale.void}\n${finale.detail}\n${miniGame.gameOverPitch}`;
465465
};
466466
const formatMainGameOver = () => {
467467
const { finale } = pickGameOverFinale();
468-
if (!finale) return `${miniGame.voidGameOverTitle}${stars}\nyou ran out of lives.`;
469-
return `${finale.title}${stars}\n${finale.main}\n${finale.detail}`;
468+
if (!finale)
469+
return `${miniGame.voidGameOverTitle}${stars}\nyou ran out of lives.\n${miniGame.gameOverPitch}`;
470+
return `${finale.title}${stars}\n${finale.main}\n${finale.detail}\n${miniGame.gameOverPitch}`;
470471
};
471472
type PlayerPose = 'none' | 'hit' | 'shrink' | 'grow' | 'stomp';
472473
let playerPose: PlayerPose = 'none';

src/scripts/hero-field.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/* =================================================================
2+
Hero "digital commons" — a lazy, GPU-driven generative WebGL field
3+
that lives BEHIND the < > mark. This is the one deliberate spectacle
4+
(the owner priced the perf trade). It is an ENHANCEMENT ONLY:
5+
· no WebGL / reduced-motion / no-JS / automation → never loads,
6+
and the static < > mark + breathing gold halo remain as the poster.
7+
Kept tiny (a fullscreen-quad fragment shader, DPR-capped, fps-capped,
8+
paused off-screen) so the trade is as small as it can be.
9+
================================================================= */
10+
11+
type Controller = { start: () => void; stop: () => void; destroy: () => void };
12+
13+
const VERT = `attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }`;
14+
15+
const FRAG = `
16+
precision highp float;
17+
uniform vec2 u_res;
18+
uniform float u_time;
19+
uniform vec2 u_mouse; // -1..1
20+
uniform float u_dark; // 1 dark, 0 light
21+
uniform float u_fund; // 0..1 share of the launch goal funded
22+
23+
float hash21(vec2 p){ p = fract(p*vec2(123.34,345.45)); p += dot(p, p+34.345); return fract(p.x*p.y); }
24+
vec2 hash22(vec2 p){ float n = sin(dot(p, vec2(41.0, 289.0))); return fract(vec2(262144.0, 32768.0)*n); }
25+
float noise(vec2 p){ vec2 i=floor(p), f=fract(p); vec2 u=f*f*(3.0-2.0*f);
26+
float a=hash21(i), b=hash21(i+vec2(1.0,0.0)), c=hash21(i+vec2(0.0,1.0)), d=hash21(i+vec2(1.0,1.0));
27+
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y); }
28+
float fbm(vec2 p){ float s=0.0, a=0.5; for(int i=0;i<5;i++){ s+=a*noise(p); p*=2.02; a*=0.5; } return s; }
29+
30+
void main(){
31+
vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / u_res.y;
32+
uv += u_mouse * 0.05;
33+
float t = u_time * 0.045;
34+
35+
// slow domain-warped energy — "the commons is alive"
36+
vec2 q = uv * 2.1;
37+
float warp = fbm(q + vec2(t, -t*0.6));
38+
float flow = fbm(q + warp*1.4 + vec2(-t*0.5, t*0.35));
39+
40+
// layered constellation of drifting nodes (dependencies / maintainers / stars)
41+
// + thin links between near nodes in a cell — a living dependency network.
42+
float pts = 0.0;
43+
float gold = 0.0;
44+
float links = 0.0;
45+
for(int L=0; L<4; L++){
46+
float fl = float(L);
47+
float sc = 5.5 + fl*6.5;
48+
vec2 gp = uv*sc + vec2(t*(0.32+fl*0.16), t*0.11 + fl*1.7);
49+
vec2 cell = floor(gp);
50+
vec2 f = fract(gp) - 0.5;
51+
// this cell's node + the neighbour node to build a short link
52+
vec2 rnd = hash22(cell);
53+
vec2 rnd2 = hash22(cell + vec2(1.0, 0.0));
54+
float layerFade = 1.0 / (1.0 + fl*0.5);
55+
if (rnd.x >= 0.42) {
56+
vec2 off = (rnd - 0.5) * 0.74;
57+
float d = length(f - off);
58+
float tw = 0.5 + 0.5*sin(u_time*(0.7+rnd.x*2.4) + rnd.y*6.2831);
59+
float pt = smoothstep(0.085, 0.0, d) * tw * layerFade;
60+
pts += pt;
61+
if (rnd.y > 0.8) gold += pt; // a few nodes are patron-gold
62+
// link to the node one cell to the right, if it exists
63+
if (rnd2.x >= 0.42) {
64+
vec2 a = off;
65+
vec2 b = (rnd2 - 0.5)*0.74 + vec2(1.0, 0.0);
66+
vec2 pa = f - a, ba = b - a;
67+
float h = clamp(dot(pa, ba)/dot(ba, ba), 0.0, 1.0);
68+
float dl = length(pa - ba*h);
69+
links += smoothstep(0.02, 0.0, dl) * 0.5 * layerFade;
70+
}
71+
}
72+
}
73+
74+
float r = length(uv);
75+
float core = smoothstep(1.2, 0.0, r); // concentrate toward the < > mark
76+
float vig = smoothstep(1.3, 0.12, r); // fade to transparent at the edges
77+
78+
// Patron nodes: a few gold nodes orbit and periodically FIRE a bright gold link
79+
// into the < > core — a legible "funding the commons" gesture, so the hero reads
80+
// on first glance rather than only on a second look.
81+
float patron = 0.0;
82+
float beam = 0.0;
83+
for(int i=0; i<4; i++){
84+
float fi = float(i);
85+
// more patron nodes light + fire as the launch goal fills (live funding %)
86+
float active = smoothstep(fi*0.25 - 0.12, fi*0.25 + 0.12, u_fund + 0.18);
87+
float ang = fi*1.5708 + t*0.22;
88+
float rad = 0.62 + 0.1*sin(t*0.8 + fi*1.3);
89+
vec2 pn = vec2(cos(ang), sin(ang)) * rad;
90+
float pulse = pow(0.5 + 0.5*sin(u_time*0.8 + fi*2.1), 8.0); // sharp + infrequent
91+
patron += smoothstep(0.06, 0.0, length(uv - pn)) * (0.3 + pulse*1.4) * active;
92+
vec2 pa = uv - pn, ba = -pn;
93+
float h = clamp(dot(pa, ba)/dot(ba, ba), 0.0, 1.0);
94+
beam += smoothstep(0.013, 0.0, length(pa - ba*h)) * pulse * (1.0 - h*0.25) * active;
95+
}
96+
97+
vec3 cGreen = vec3(0.24, 0.88, 0.48);
98+
vec3 cGold = vec3(0.91, 0.72, 0.29);
99+
100+
vec3 col = cGreen*(pts-gold)*1.9 + cGold*gold*2.1 + cGreen*links*1.1;
101+
col += cGold*(patron*1.5 + beam*1.4); // patrons + funding beams
102+
col += cGreen*flow*0.14*core; // living energy
103+
col += cGold*core*0.07; // soft central gilt
104+
105+
float alpha = (pts*1.9 + links*0.9 + patron*1.4 + beam*1.15 + flow*0.13*core + core*0.06) * vig;
106+
107+
// light theme: no glow reads on parchment, so ink the field down + soften
108+
vec3 cInk = vec3(0.10, 0.10, 0.07);
109+
col = mix(mix(cInk, cGold*0.55, gold*0.9) * (pts + core*0.25), col, u_dark);
110+
alpha *= mix(0.42, 1.0, u_dark);
111+
112+
gl_FragColor = vec4(col, clamp(alpha, 0.0, 1.0));
113+
}`;
114+
115+
function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null {
116+
const sh = gl.createShader(type);
117+
if (!sh) return null;
118+
gl.shaderSource(sh, src);
119+
gl.compileShader(sh);
120+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
121+
gl.deleteShader(sh);
122+
return null;
123+
}
124+
return sh;
125+
}
126+
127+
export function initHeroField(canvas: HTMLCanvasElement): Controller | null {
128+
const gl = (canvas.getContext('webgl', { alpha: true, antialias: true, premultipliedAlpha: false }) ||
129+
canvas.getContext('experimental-webgl', { alpha: true })) as WebGLRenderingContext | null;
130+
if (!gl) return null;
131+
132+
const vs = compile(gl, gl.VERTEX_SHADER, VERT);
133+
const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);
134+
if (!vs || !fs) return null;
135+
const prog = gl.createProgram();
136+
if (!prog) return null;
137+
gl.attachShader(prog, vs);
138+
gl.attachShader(prog, fs);
139+
gl.linkProgram(prog);
140+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;
141+
gl.useProgram(prog);
142+
143+
const buf = gl.createBuffer();
144+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
145+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
146+
const loc = gl.getAttribLocation(prog, 'p');
147+
gl.enableVertexAttribArray(loc);
148+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
149+
150+
const uRes = gl.getUniformLocation(prog, 'u_res');
151+
const uTime = gl.getUniformLocation(prog, 'u_time');
152+
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
153+
const uDark = gl.getUniformLocation(prog, 'u_dark');
154+
155+
gl.enable(gl.BLEND);
156+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
157+
158+
const mouse = { x: 0, y: 0 };
159+
const isDark = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 1 : 0);
160+
161+
const resize = () => {
162+
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
163+
const w = Math.max(1, Math.round(canvas.clientWidth * dpr));
164+
const h = Math.max(1, Math.round(canvas.clientHeight * dpr));
165+
if (canvas.width !== w || canvas.height !== h) {
166+
canvas.width = w;
167+
canvas.height = h;
168+
}
169+
gl.viewport(0, 0, canvas.width, canvas.height);
170+
};
171+
172+
const draw = (timeSec: number) => {
173+
gl.uniform2f(uRes, canvas.width, canvas.height);
174+
gl.uniform1f(uTime, timeSec);
175+
gl.uniform2f(uMouse, mouse.x, mouse.y);
176+
gl.uniform1f(uDark, isDark());
177+
gl.drawArrays(gl.TRIANGLES, 0, 3);
178+
};
179+
180+
let raf = 0;
181+
let running = false;
182+
let startMs = 0;
183+
let last = 0;
184+
const frameMs = 1000 / 40; // fps cap — keep the GPU/CPU cost bounded
185+
186+
const onPointer = (e: PointerEvent) => {
187+
const r = canvas.getBoundingClientRect();
188+
mouse.x = ((e.clientX - r.left) / r.width - 0.5) * 2;
189+
mouse.y = ((e.clientY - r.top) / r.height - 0.5) * -2;
190+
};
191+
192+
const loop = (nowMs: number) => {
193+
if (!running) return;
194+
raf = requestAnimationFrame(loop);
195+
if (nowMs - last < frameMs) return;
196+
last = nowMs;
197+
if (!startMs) startMs = nowMs;
198+
resize();
199+
draw((nowMs - startMs) / 1000);
200+
};
201+
202+
const start = () => {
203+
if (running) return;
204+
running = true;
205+
startMs = 0;
206+
last = 0;
207+
window.addEventListener('pointermove', onPointer, { passive: true });
208+
raf = requestAnimationFrame(loop);
209+
};
210+
const stop = () => {
211+
running = false;
212+
if (raf) cancelAnimationFrame(raf);
213+
raf = 0;
214+
window.removeEventListener('pointermove', onPointer);
215+
};
216+
const destroy = () => {
217+
stop();
218+
gl.deleteProgram(prog);
219+
gl.deleteShader(vs);
220+
gl.deleteShader(fs);
221+
gl.deleteBuffer(buf);
222+
};
223+
224+
// paint one deterministic frame immediately (also the reduced-motion poster)
225+
resize();
226+
draw(6.0);
227+
228+
return { start, stop, destroy };
229+
}

0 commit comments

Comments
 (0)