Skip to content

Commit 096051f

Browse files
committed
Latent field animation. Copy tweaks
1 parent c2c3936 commit 096051f

3 files changed

Lines changed: 250 additions & 9 deletions

File tree

src/components/Header.astro

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<span class="text-muted">/2026 · speakers</span>
1313
</a>
1414
<nav class="flex items-center gap-6 text-sm">
15-
<a href="https://lisbonai.xyz/about">About</a>
1615
<a href="https://lisbonai.xyz">Main site &rarr;</a>
1716
</nav>
1817
</header>

src/components/LatentField.astro

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
---
2+
interface Props {
3+
class?: string;
4+
}
5+
6+
const { class: className } = Astro.props;
7+
---
8+
9+
<canvas data-latent-field class:list={["pointer-events-none block", className]}
10+
></canvas>
11+
12+
<script>
13+
function init(canvas: HTMLCanvasElement) {
14+
const gl = canvas.getContext("webgl", {
15+
alpha: true,
16+
antialias: false,
17+
premultipliedAlpha: false,
18+
});
19+
20+
if (!gl) {
21+
canvas.style.display = "none";
22+
return;
23+
}
24+
25+
const reduceMotion = window.matchMedia(
26+
"(prefers-reduced-motion: reduce)",
27+
).matches;
28+
29+
const vsSrc = `
30+
attribute vec2 aPos;
31+
void main() {
32+
gl_Position = vec4(aPos, 0.0, 1.0);
33+
}
34+
`;
35+
36+
const fsSrc = `
37+
precision highp float;
38+
uniform vec2 uResolution;
39+
uniform float uTileSize;
40+
uniform float uTime;
41+
uniform float uPitch;
42+
43+
vec3 mod289_3(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
44+
vec4 mod289_4(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
45+
vec4 permute(vec4 x) { return mod289_4(((x * 34.0) + 1.0) * x); }
46+
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
47+
48+
float snoise(vec3 v) {
49+
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
50+
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
51+
vec3 i = floor(v + dot(v, C.yyy));
52+
vec3 x0 = v - i + dot(i, C.xxx);
53+
vec3 g = step(x0.yzx, x0.xyz);
54+
vec3 l = 1.0 - g;
55+
vec3 i1 = min(g.xyz, l.zxy);
56+
vec3 i2 = max(g.xyz, l.zxy);
57+
vec3 x1 = x0 - i1 + C.xxx;
58+
vec3 x2 = x0 - i2 + C.yyy;
59+
vec3 x3 = x0 - D.yyy;
60+
i = mod289_3(i);
61+
vec4 p = permute(permute(permute(
62+
i.z + vec4(0.0, i1.z, i2.z, 1.0))
63+
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
64+
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
65+
float n_ = 0.142857142857;
66+
vec3 ns = n_ * D.wyz - D.xzx;
67+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
68+
vec4 x_ = floor(j * ns.z);
69+
vec4 y_ = floor(j - 7.0 * x_);
70+
vec4 x = x_ * ns.x + ns.yyyy;
71+
vec4 y = y_ * ns.x + ns.yyyy;
72+
vec4 h = 1.0 - abs(x) - abs(y);
73+
vec4 b0 = vec4(x.xy, y.xy);
74+
vec4 b1 = vec4(x.zw, y.zw);
75+
vec4 s0 = floor(b0) * 2.0 + 1.0;
76+
vec4 s1 = floor(b1) * 2.0 + 1.0;
77+
vec4 sh = -step(h, vec4(0.0));
78+
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
79+
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
80+
vec3 p0 = vec3(a0.xy, h.x);
81+
vec3 p1 = vec3(a0.zw, h.y);
82+
vec3 p2 = vec3(a1.xy, h.z);
83+
vec3 p3 = vec3(a1.zw, h.w);
84+
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
85+
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
86+
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
87+
m = m * m;
88+
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
89+
}
90+
91+
float fbm(vec3 p) {
92+
return snoise(p) * 0.65 + snoise(p * 2.1 + 5.0) * 0.35;
93+
}
94+
95+
void main() {
96+
vec2 frag = gl_FragCoord.xy;
97+
98+
vec2 tileSize = vec2(uTileSize);
99+
vec2 tileId = floor(frag / tileSize);
100+
vec2 inTile = frag - tileId * tileSize;
101+
102+
float gap = uTileSize * 0.05;
103+
vec2 innerSize = tileSize - 2.0 * gap;
104+
vec2 innerPos = inTile - gap;
105+
if (innerPos.x < 0.0 || innerPos.y < 0.0 ||
106+
innerPos.x > innerSize.x || innerPos.y > innerSize.y) {
107+
gl_FragColor = vec4(0.0);
108+
return;
109+
}
110+
111+
vec2 cell = innerPos / uPitch;
112+
vec2 cellId = floor(cell);
113+
vec2 cellCenter = (cellId + 0.5) * uPitch;
114+
vec2 d = innerPos - cellCenter;
115+
float dist = length(d);
116+
117+
if (cellCenter.x < 0.0 || cellCenter.y < 0.0 ||
118+
cellCenter.x > innerSize.x || cellCenter.y > innerSize.y) {
119+
gl_FragColor = vec4(0.0);
120+
return;
121+
}
122+
123+
vec2 dotUv = cellCenter / innerSize;
124+
vec2 tileSeed = tileId * 23.7 + vec2(3.1, 11.5);
125+
float field = fbm(vec3(dotUv * 2.4 + tileSeed, uTime * 0.08));
126+
127+
float threshold = 0.05;
128+
if (field > threshold) {
129+
gl_FragColor = vec4(0.0);
130+
return;
131+
}
132+
133+
float dotR = uPitch * 0.18;
134+
float alpha = smoothstep(dotR + 0.6, dotR - 0.6, dist);
135+
if (alpha < 0.01) {
136+
gl_FragColor = vec4(0.0);
137+
return;
138+
}
139+
140+
float edgeStart = threshold - 0.22;
141+
float edgeFactor = smoothstep(edgeStart, threshold, field);
142+
143+
vec3 cDot = vec3(0.85, 0.60, 0.92);
144+
vec3 cEdge = vec3(0.96, 0.34, 0.42);
145+
vec3 col = mix(cDot, cEdge, edgeFactor);
146+
147+
gl_FragColor = vec4(col, alpha);
148+
}
149+
`;
150+
151+
function compile(type: number, src: string) {
152+
const s = gl!.createShader(type)!;
153+
gl!.shaderSource(s, src);
154+
gl!.compileShader(s);
155+
if (!gl!.getShaderParameter(s, gl!.COMPILE_STATUS)) {
156+
console.error(gl!.getShaderInfoLog(s));
157+
}
158+
return s;
159+
}
160+
161+
const program = gl.createProgram()!;
162+
gl.attachShader(program, compile(gl.VERTEX_SHADER, vsSrc));
163+
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fsSrc));
164+
gl.linkProgram(program);
165+
gl.useProgram(program);
166+
167+
const quad = new Float32Array([
168+
-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,
169+
]);
170+
const buf = gl.createBuffer();
171+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
172+
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
173+
174+
const aPos = gl.getAttribLocation(program, "aPos");
175+
gl.enableVertexAttribArray(aPos);
176+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
177+
178+
const uResolution = gl.getUniformLocation(program, "uResolution");
179+
const uTileSize = gl.getUniformLocation(program, "uTileSize");
180+
const uTime = gl.getUniformLocation(program, "uTime");
181+
const uPitch = gl.getUniformLocation(program, "uPitch");
182+
183+
gl.enable(gl.BLEND);
184+
gl.blendFuncSeparate(
185+
gl.SRC_ALPHA,
186+
gl.ONE_MINUS_SRC_ALPHA,
187+
gl.ONE,
188+
gl.ONE_MINUS_SRC_ALPHA,
189+
);
190+
gl.clearColor(0, 0, 0, 0);
191+
192+
function resize() {
193+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
194+
const rect = canvas.getBoundingClientRect();
195+
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
196+
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
197+
gl!.viewport(0, 0, canvas.width, canvas.height);
198+
199+
const isNarrow = window.matchMedia("(max-width: 768px)").matches;
200+
const tilesAcrossShortSide = isNarrow ? 2 : 3;
201+
const tileSize =
202+
Math.min(canvas.width, canvas.height) / tilesAcrossShortSide;
203+
const pitch = Math.max(4, Math.round(tileSize / 50));
204+
205+
gl!.uniform2f(uResolution, canvas.width, canvas.height);
206+
gl!.uniform1f(uTileSize, tileSize);
207+
gl!.uniform1f(uPitch, pitch);
208+
}
209+
210+
resize();
211+
window.addEventListener("resize", resize);
212+
213+
let running = true;
214+
document.addEventListener("visibilitychange", () => {
215+
running = !document.hidden;
216+
if (running) requestAnimationFrame(frame);
217+
});
218+
219+
const start = performance.now();
220+
221+
function frame() {
222+
if (!running) return;
223+
const t = (performance.now() - start) / 1000;
224+
gl!.clear(gl!.COLOR_BUFFER_BIT);
225+
gl!.uniform1f(uTime, reduceMotion ? 0 : t);
226+
gl!.drawArrays(gl!.TRIANGLES, 0, 6);
227+
if (!reduceMotion) requestAnimationFrame(frame);
228+
}
229+
230+
requestAnimationFrame(frame);
231+
}
232+
233+
document
234+
.querySelectorAll<HTMLCanvasElement>("canvas[data-latent-field]")
235+
.forEach(init);
236+
</script>

src/pages/index.astro

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import Layout from "../layouts/Layout.astro";
33
import Header from "../components/Header.astro";
44
import Footer from "../components/Footer.astro";
55
import PastSpeakers from "../components/PastSpeakers.astro";
6+
import LatentField from "../components/LatentField.astro";
67
78
const eventDetails = [
89
{ label: "Dates", value: "September 23 – 24, 2026" },
910
{ label: "Venue", value: "Centro Champalimaud, Belém · Lisbon" },
10-
{ label: "Attendees", value: "~400 builders, in person" },
11+
{ label: "Attendees", value: "~400 attendees in person" },
1112
{ label: "Speakers", value: "Aiming for around 20" },
1213
{
1314
label: "Format",
@@ -89,11 +90,16 @@ const faq = [
8990
<Header />
9091

9192
<!-- Hero -->
92-
<section class="relative">
93+
<section class="relative overflow-hidden">
94+
<LatentField class="absolute inset-0 h-full w-full" />
9395
<div
94-
class="mx-auto flex min-h-[70vh] max-w-6xl flex-col justify-end px-8 pb-28 pt-16 md:pt-20"
96+
class="relative z-10 mx-auto flex min-h-[70vh] max-w-6xl flex-col justify-end px-8 pb-28 pt-16 md:pt-20"
9597
>
96-
<div class="small-headline mb-4">/lisbon-ai-2026 · speakers</div>
98+
<div class="small-headline mb-4">
99+
<span class="bg-slate-100 rounded-lg px-2 py-1">
100+
/lisbon-ai-2026 · speakers
101+
</span>
102+
</div>
97103
<h1 class="big-headline">
98104
WELCOME LISBONAI 2026<br />
99105
SPEAKERS <span class="text-accent">{"❤️"}</span>
@@ -113,8 +119,8 @@ const faq = [
113119
<p>
114120
We know saying yes to a conference talk costs real time and
115121
real travel, and we don't take that lightly. Thank you for
116-
trusting us with that. We'll do our best to make sure you
117-
have a great experience.
122+
trusting. We'll do our best to make sure you have a great
123+
experience.
118124
</p>
119125
<p>
120126
If any of the information below is unclear, ambiguous, or
@@ -133,7 +139,7 @@ const faq = [
133139

134140
<!-- 2026 act break -->
135141
<section class="border-t border-ink/10">
136-
<div class="mx-auto max-w-6xl px-6 py-24 md:py-32 md:pb-12">
142+
<div class="mx-auto max-w-6xl px-6 py-24 pb-0 md:py-32 md:pb-12">
137143
<div class="small-headline mb-6">/2026-edition</div>
138144
<h2 class="big-headline">EVERYTHING ABOUT 2026</h2>
139145
</div>
@@ -290,7 +296,7 @@ const faq = [
290296
href="https://discord.com/invite/ExnMf8wTPU"
291297
class="rounded-full border border-ink px-6 py-3 hover:border-accent hover:text-accent transition-colors"
292298
>
293-
Discord
299+
Our discord
294300
</a>
295301
</div>
296302
<p class="mt-6 font-mono text-xs uppercase text-faint">

0 commit comments

Comments
 (0)