1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
|
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Latens</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 32 32%27%3E%3Crect width=%2732%27 height=%2732%27 fill=%27%23050505%27/%3E%3Ccircle cx=%2716%27 cy=%2716%27 r=%275%27 fill=%27%23e8b679%27/%3E%3C/svg%3E">
<meta name="description" content="The extraction layer of latent variables for AGI infrastructure.">
<meta name="theme-color" content="#050505">
<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=Hanken+Grotesk:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;1,6..72,300&display=swap" rel="stylesheet">
<script defer src="https://analytics.ugmi.ca/script.js" data-website-id="73f90368-e4c7-43d3-adbe-2ceb0e44853d"></script>
<style>
:root{
--ground:#050505;
--ink:#f2efe9;
--ink-soft:rgba(242,239,233,.72);
--ink-faint:rgba(242,239,233,.28);
--disk:#e8b679;
--gutter:clamp(16px,4.4vw,56px);
--serif:'Newsreader',Georgia,'Times New Roman',serif;
--sans:'Hanken Grotesk','Helvetica Neue',sans-serif;
--ease:cubic-bezier(.16,1,.3,1);
color-scheme:dark;
}
*{box-sizing:border-box}
html{background:var(--ground);scrollbar-width:thin;scrollbar-color:var(--ink-faint) transparent}
html,body{margin:0;overflow-x:hidden;overscroll-behavior-y:none}
body{background:var(--ground);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.45;-webkit-font-smoothing:antialiased}
::selection{background:var(--disk);color:var(--ground)}
#track{height:900vh}
#stage{position:fixed;inset:0;height:100lvh;overflow:hidden;background:var(--ground)}
/* SCROLL FIX (css) start: scroll an inner full-screen box, not the document, so phone browser toolbars never show, hide or resize the screen */
html,body{height:100%;overflow:hidden}
#scroller{position:fixed;inset:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;touch-action:pan-y;outline:none;scrollbar-width:thin;scrollbar-color:var(--ink-faint) transparent}
#track{height:800%} /* total scroll stays the same: the pinned #stage takes the first screen */
#stage{position:sticky;inset:auto;top:0;height:100vh;height:100dvh} /* pinned inside #scroller: a fixed element would pass wheel and touch scrolls to the page, which cannot scroll */
#stage .layer{position:absolute}
#load{pointer-events:none}
/* SCROLL FIX (css) end */
#gl{position:absolute;inset:0;width:100%;height:100%;display:block;opacity:0;transition:opacity .8s var(--ease)}
#gl.on{opacity:1}
.layer{position:fixed;left:0;right:0;padding:0 var(--gutter);pointer-events:none;opacity:0;will-change:opacity,transform}
.layer p{margin:0}
#s1,#s6{top:clamp(44px,13svh,150px);text-align:center}
#s1{opacity:1}
#s1 h1,#s6 .name{font-family:var(--serif);font-weight:300;font-size:clamp(3.25rem,9vw,6rem);line-height:1;letter-spacing:-.02em;margin:0}
#s1 p,#s6 .one{margin:clamp(14px,2.2svh,22px) auto 0;max-width:34ch;color:var(--ink-soft);font-size:clamp(15px,1.25vw,17px);text-wrap:balance}
.line{font-family:var(--serif);font-weight:300;font-size:clamp(1.55rem,2.7vw,2.4rem);line-height:1.12;letter-spacing:-.01em;max-width:17ch;text-wrap:balance}
#s2{bottom:calc(13svh + env(safe-area-inset-bottom))}
#s3{top:50%;margin-top:-1.2em}
#s4{top:clamp(44px,14svh,150px);display:flex;justify-content:flex-end}
#s4 .line{text-align:right}
#s5{bottom:calc(13svh + env(safe-area-inset-bottom));text-align:center}
#s5 .line{margin:0 auto}
.cta{display:inline-block;margin-top:clamp(20px,3.4svh,32px);padding:14px 24px;background:var(--ink);color:var(--ground);font:500 15px/1 var(--sans);letter-spacing:.01em;text-decoration:none;border-radius:2px;transition:background-color .2s var(--ease)}
.cta:hover{background:var(--disk)}
.cta:focus-visible{outline:1px solid var(--ink);outline-offset:4px}
#load{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom));transform:translateX(-50%);width:min(200px,50vw);text-align:center;color:var(--ink-soft);font-size:13px;letter-spacing:.02em;opacity:0;transition:opacity .4s var(--ease);pointer-events:none}
#load.on{opacity:1}
#load i{display:block;height:1px;margin-top:9px;background:var(--ink-faint);position:relative;overflow:hidden}
#load b{position:absolute;inset:0;background:var(--ink);transform-origin:0 50%;transform:scaleX(0);transition:transform .3s var(--ease)}
@media (max-width:700px){
#s3{top:auto;margin-top:0;bottom:calc(13svh + env(safe-area-inset-bottom))}
#s4{justify-content:center}
#s4 .line{text-align:center}
#s2 .line,#s3 .line{max-width:16ch}
}
@media (prefers-reduced-motion:reduce){#gl,#load,#load b{transition:none}}
</style>
</head>
<body>
<div id="scroller" tabindex="-1"><!-- SCROLL FIX (markup): everything that was in body now sits inside #scroller -->
<div id="stage" aria-hidden="false">
<canvas id="gl" aria-hidden="true"></canvas>
<header class="layer" id="s1">
<h1>Latens</h1>
<p>The extraction layer of latent variables for AGI infrastructure.</p>
</header>
<section class="layer" id="s2"><p class="line">Most of what moves a person is never said out loud.</p></section>
<section class="layer" id="s3"><p class="line">We learn who someone is from what they do, day after day.</p></section>
<section class="layer" id="s4"><p class="line">We are building that understanding for every person on Earth.</p></section>
<section class="layer" id="s5"><p class="line">Every AI system that works for people will need this layer.</p></section>
<section class="layer" id="s6">
<p class="name" aria-hidden="true">Latens</p>
<p class="one" aria-hidden="true">The extraction layer of latent variables for AGI infrastructure.</p>
<a class="cta" href="mailto:hello@latens.ca" data-umami-event="contact">Get in touch</a>
</section>
</div>
<div id="load" role="status"><span>Loading</span><i><b></b></i></div>
<div id="track"></div>
</div>
<script id="fs" type="x-shader/x-fragment">#version 300 es
precision highp float;
uniform vec2 uRes;
uniform float uTime, uOct;
uniform vec2 uMouse;
uniform vec3 uQ, uP, uA, uB, uC; // uQ: the closing nebula around Earth, in its own units. // uP: pupil units at screen center, z per p unit. uA/uB/uC: image uv at screen center, z per p unit
uniform float uCosA, uAA, uBA, uEA, uQA, uCore, uDim;
uniform sampler2D tA, tB, tC;
uniform vec4 uDilA, uDilB; // eye in still A and still B: pupil center xy, iris radius, pupil radius in the image
out vec4 outColor;
// measured on the stills, in uv with y down
const vec2 C_EARTH = vec2(.496,.484);// Earth center in still C
float h21(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }
vec2 h22(vec2 p){ float n=h21(p); return vec2(n,h21(p+n+17.17)); }
float vnoise(vec2 p){ vec2 i=floor(p), f=fract(p); vec2 u=f*f*(3.-2.*f);
return mix(mix(h21(i),h21(i+vec2(1,0)),u.x),mix(h21(i+vec2(0,1)),h21(i+vec2(1,1)),u.x),u.y); }
float hP(vec2 i,float P){ i.x=mod(i.x,P); return h21(i); }
float vnoiseP(vec2 p,float P){ vec2 i=floor(p), f=fract(p); vec2 u=f*f*(3.-2.*f);
return mix(mix(hP(i,P),hP(i+vec2(1,0),P),u.x),mix(hP(i+vec2(0,1),P),hP(i+vec2(1,1),P),u.x),u.y); }
float fbm(vec2 p){ float a=.5,s=0.; for(int i=0;i<5;i++){ s+=a*vnoise(p); p=p*2.03+vec2(1.7,9.2); a*=.5; } return s; }
const mat2 OR=mat2(.8,.6,-.6,.8);
float fbmN(vec2 p){ float a=.5,s=0.,w=0.; for(int i=0;i<6;i++){ if(float(i)>=uOct) break; s+=a*vnoise(p); w+=a; p=OR*p*2.04+vec2(1.7,9.2); a*=.5; } return s/w; }
vec3 stars(vec2 q,float px,float seed){
vec3 c=vec3(0);
for(int l=0;l<4;l++){
float fl=float(l);
float g=3.*pow(2.4,fl);
float vis=1.-smoothstep(.12,.45,g*px);
if(vis<=0.) continue;
vec2 cq=q*g, id=floor(cq);
vec2 r=h22(id+vec2(seed+fl*17.,3.1));
if(r.x<.3) continue;
vec2 sp=id+.15+.7*h22(id+vec2(9.7,seed+fl*5.));
float d=length(cq-sp)/g;
float rad=1.15*px;
float b=pow(h21(id+seed*1.3+fl),7.)*2.6+.12;
b*=.8+.2*sin(uTime*(.8+r.y*2.)+r.x*40.);
vec3 tint=mix(vec3(.74,.82,1.),vec3(1.,.84,.68),h21(id+4.2));
c+=tint*b*vis*exp(-d*d/(rad*rad));
}
return c;
}
// nebula in pupil units: the core sits at q=0, the pupil edge at r=1
vec3 cosmos(vec2 q,float px,float ck){
float t=uTime;
vec2 p=(q+uMouse*.012)*3.6;
float r=length(p), th=atan(p.y,p.x);
float sw=t*.02+1.4/(r+.5); // inner gas turns faster than the outer arms
float cs=cos(sw), sn=sin(sw);
vec2 w=mat2(cs,-sn,sn,cs)*p;
vec2 o=vec2(fbmN(w*1.2+vec2(0.,t*.008)), fbmN(w*1.2+vec2(5.2,1.3)));
float n=fbmN(w*2.2+o*1.6);
float ph=2.*th-4.2*log(r+.05)+t*.04+2.2*(o.y-.5); // two logarithmic spiral arms
float arm=pow(.5+.5*cos(ph),2.6);
float lane=pow(.5+.5*cos(ph-1.05),6.)*smoothstep(.12,.5,r)*smoothstep(.3,.7,fbmN(w*3.+o*2.));
float fall=exp(-r*r*.16);
float dens=smoothstep(.2,.75,n)*(.12+.88*arm)*fall;
vec3 blue=vec3(.06,.10,.21), rust=vec3(.40,.20,.11);
vec3 gas=mix(blue,rust,smoothstep(.35,.75,o.x)*smoothstep(1.6,.25,r));
vec3 col=gas*dens*3.*(1.-.75*lane);
col+=vec3(.012,.016,.03)*fall;
float core=exp(-r*r*16.)*.95+exp(-r*3.)*.22;
col+=vec3(1.,.8,.6)*core*(1.-.45*lane)*ck;
col+=vec3(1.,.95,.88)*exp(-r*r*260.)*.5*ck;
vec3 st=stars(q+uMouse*.03,px,1.)+stars(q*1.7+uMouse*.012,px*1.7,7.)*.5;
col+=st*(1.-.7*smoothstep(.08,.5,dens))*(1.-.6*lane);
return 1.-exp(-col*1.25);
}
// widen the pupil: inside the iris, pull samples toward the center so the pupil grows to PUP_TO of the iris radius and the iris edge stays put
const float PUP_TO=.44;
vec2 dilate(vec2 uv,vec4 D){
vec2 d=uv-D.xy; float l=length(d), rq=PUP_TO*D.z;
if(l>=D.z || l<1e-6) return uv;
float s=l<rq ? l*D.w/rq : D.w+(l-rq)*(D.z-D.w)/(D.z-rq);
return D.xy+d*(s/l);
}
vec2 img(vec3 U,vec2 p){ return U.xy+vec2(p.x,-p.y)*U.z; }
bool inside(vec2 uv){ return all(greaterThanEqual(uv,vec2(0))) && all(lessThanEqual(uv,vec2(1))); }
void main(){
float k=.5*min(uRes.x,uRes.y);
vec2 p=(gl_FragCoord.xy-.5*uRes)/k;
float pk=1./k;
vec3 col=vec3(0);
// Earth, cut to its sphere, sits in the core of a second copy of the opening nebula
float me=0.; vec3 ec=vec3(0);
if(uEA>0.){
vec2 uv=img(uC,p);
if(inside(uv)){ me=uEA*smoothstep(.425,.408,length(uv-C_EARTH)); ec=texture(tC,uv).rgb; }
}
if(me<1. && uQA>0.){
vec2 q=uQ.xy+p*uQ.z;
float r=length(q);
if(r<1.) col=cosmos(q,pk*uQ.z,uCore)*uQA*smoothstep(1.,.86,r);
}
col=mix(col,ec,me);
col*=1.-.85*uDim;
if(uBA>0.){
vec2 uv=img(uB,p);
if(inside(uv)){
float m=smoothstep(.5,.34,length(uv-.5));
col=mix(col,texture(tB,dilate(uv,uDilB)).rgb,uBA*m);
}
}
if(uAA>0.){
vec2 uv=img(uA,p);
if(inside(uv)){
float m=smoothstep(.49,.36,length(uv-.5)); // still A is cut from still B, so its soft edge blends into the same pixels
col=mix(col,texture(tA,dilate(uv,uDilA)).rgb,uAA*m);
}
}
if(uCosA>0.){
vec2 q=uP.xy+p*uP.z;
float r=length(q);
if(r<1.){
float m=smoothstep(1.,.86,r);
col=mix(col,cosmos(q,pk*uP.z,1.),uCosA*m);
}
}
vec2 v=gl_FragCoord.xy/uRes-.5;
col*=1.-.35*dot(v,v);
col+=(h21(gl_FragCoord.xy+fract(uTime))-.5)/255.;
outColor=vec4(col,1.);
}
</script>
<script>
(function(){
'use strict';
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
var layers = [1,2,3,4,5,6].map(function(i){ return document.getElementById('s'+i); });
var cta = document.querySelector('.cta');
var load = document.getElementById('load');
var bar = load.querySelector('b');
var canvas = document.getElementById('gl');
var shown = false;
// World units: Earth centered at 0 with radius .06.
// Each still is placed by its world center and world width. The numbers below were measured on the stills.
var EARTH_UV=[.496,.484], EARTH_R=.414; // still C: Earth center and radius, in image widths
var FACE_IRIS=.0190; // still B: iris radius of the lit eye
var A_CROP=[.338068,.395241,.113991]; // still A is a square crop of still B: left, top and width in still B uv
var A_PUP=[.5034,.5001], A_PR=.0600; // still A: pupil center and pupil radius
function at(img,uv){ return [img.c[0]+(uv[0]-.5)*img.s, img.c[1]-(uv[1]-.5)*img.s]; }
var W={};
W.C={s:.06/EARTH_R}; W.C.c=[(.5-EARTH_UV[0])*W.C.s, -(.5-EARTH_UV[1])*W.C.s];
W.B={s:.30*W.C.s, c:[0,0]};
W.A={s:A_CROP[2]*W.B.s, c:at(W.B,[A_CROP[0]+A_CROP[2]/2, A_CROP[1]+A_CROP[2]/2])};
var A_IR=FACE_IRIS/A_CROP[2], PUP_TO=.44; // iris radius in still A; the shader widens the pupil to PUP_TO of it
W.P={c:at(W.A,A_PUP), r:PUP_TO*A_IR*W.A.s};
function keys(){
return [
{a:0, b:.05, v:.32*W.P.r, c:W.P.c},
{a:.18, b:.25, v:1.0947*FACE_IRIS*W.B.s, c:W.P.c},
{a:.37, b:.44, v:.42*W.B.s, c:W.B.c},
{a:.56, b:.63, v:1.45*.06, c:[0,0]},
{a:.76, b:.82, v:.5, c:[0,0]},
{a:.95, b:1, v:.32*NEB, c:[0,0]} // same framing as the opening, so the page ends where it began
];
}
var NEB=4; // closing nebula radius in world units: Earth (radius .06) is the bright point at its core
var K = keys();
function ease(s){ return s<.5 ? 4*s*s*s : 1-Math.pow(-2*s+2,3)/2; }
function camera(t){
var i, k, n, s, v0, v1, v, w;
for(i=0;i<K.length;i++){
k=K[i];
if(t>=k.a && t<=k.b){ s=(t-k.a)/(k.b-k.a||1); return {v:k.v*(1+.07*s), c:k.c}; }
n=K[i+1];
if(n && t>k.b && t<n.a){
s=ease((t-k.b)/(n.a-k.b));
v0=k.v*1.07; v1=n.v;
v=Math.exp(Math.log(v0)+(Math.log(v1)-Math.log(v0))*s);
w=(v1-v)/(v1-v0);
return {v:v, c:[n.c[0]+(k.c[0]-n.c[0])*w, n.c[1]+(k.c[1]-n.c[1])*w]};
}
}
k=K[K.length-1]; return {v:k.v*1.07, c:k.c};
}
function ss(a,b,x){ x=Math.min(1,Math.max(0,(x-a)/(b-a))); return x*x*(3-2*x); }
function textAt(t){
var L=K.length-1, o=[1-ss(K[0].b-.01,K[0].b+.05,t), 0,0,0,0, ss(K[L].a-.055,K[L].a+.005,t)];
for(var i=1;i<L;i++){ var k=K[i]; o[i]=ss(k.a-.05,k.a-.005,t)*(1-ss(k.b+.005,k.b+.05,t)); }
for(i=0;i<=L;i++){
var el=layers[i];
el.style.opacity=o[i].toFixed(3);
el.style.transform='translate3d(0,'+((1-o[i])*10).toFixed(1)+'px,0)';
}
cta.style.pointerEvents = o[L] > .5 ? 'auto' : 'none';
layers[L].style.visibility = o[L] > .01 ? 'visible' : 'hidden';
}
// SCROLL FIX (js) start: read progress from #scroller, follow the finger directly on touch screens, resize on rotation
var scroller=document.getElementById('scroller');
var touch=matchMedia('(pointer: coarse)').matches;
function progress(){ var m=scroller.scrollHeight-scroller.clientHeight; return m>0 ? Math.min(1,Math.max(0,scroller.scrollTop/m)) : 0; }
scroller.addEventListener('scroll', function(){ target=progress(); if(touch) cur=target; }, {passive:true});
addEventListener('orientationchange', function(){ setTimeout(function(){ K=keys(); target=progress(); resize(); }, 120); });
scroller.focus({preventScroll:true}); // arrow keys and space scroll the box on desktop
// SCROLL FIX (js) end
var target=progress(), cur=target;
var mouse=[0,0], mt=[0,0];
addEventListener('pointermove', function(e){ if(e.pointerType==='mouse'){ mt[0]=e.clientX/innerWidth*2-1; mt[1]=-(e.clientY/innerHeight*2-1); } }, {passive:true});
addEventListener('resize', function(){ K=keys(); target=progress(); resize(); });
textAt(cur);
var gl, prog, U={}, tex={}, scale=1, dpr=Math.min(devicePixelRatio||1, 1.5);
function resize(){
if(!gl) return;
var w=Math.round(canvas.clientWidth*dpr*scale), h=Math.round(canvas.clientHeight*dpr*scale);
if(canvas.width!==w || canvas.height!==h){ canvas.width=w; canvas.height=h; gl.viewport(0,0,w,h); }
boxW=canvas.clientWidth; boxH=canvas.clientHeight;
}
// ASPECT FIX: phone toolbars can change the canvas box without a matching window resize event, which left the drawing buffer at the old height and squashed every stage vertically.
// Resize whenever the canvas box itself changes, from three sources, so rotation and the address bar showing or hiding always keep circles round.
var boxW=0, boxH=0;
if(window.ResizeObserver) new ResizeObserver(function(){ resize(); }).observe(canvas);
if(window.visualViewport) visualViewport.addEventListener('resize', function(){ K=keys(); target=progress(); resize(); });
function makeTex(unit){
var t=gl.createTexture(); gl.activeTexture(gl.TEXTURE0+unit); gl.bindTexture(gl.TEXTURE_2D,t);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([0,0,0,255]));
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
return t;
}
function init(){
gl=canvas.getContext('webgl2',{antialias:false,alpha:false,powerPreference:'high-performance'});
if(!gl) return;
var vs=gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs,'#version 300 es\nin vec2 a;void main(){gl_Position=vec4(a,0.,1.);}');
gl.compileShader(vs);
var fs=gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs,document.getElementById('fs').textContent);
gl.compileShader(fs);
prog=gl.createProgram(); gl.attachShader(prog,vs); gl.attachShader(prog,fs); gl.linkProgram(prog);
if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){ console.error(gl.getShaderInfoLog(fs)); gl=null; return; }
gl.useProgram(prog);
var b=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,b);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);
var loc=gl.getAttribLocation(prog,'a'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc,2,gl.FLOAT,false,0,0);
['uRes','uTime','uOct','uMouse','uQ','uP','uA','uB','uC','uCosA','uAA','uBA','uEA','uQA','uCore','uDim','tA','tB','tC','uDilA','uDilB'].forEach(function(n){ U[n]=gl.getUniformLocation(prog,n); });
['A','B','C'].forEach(function(n,i){ tex[n]=makeTex(i); gl.uniform1i(U['t'+n],i); });
gl.uniform4f(U.uDilA,A_PUP[0],A_PUP[1],A_IR,A_PR);
gl.uniform4f(U.uDilB,A_CROP[0]+A_PUP[0]*A_CROP[2],A_CROP[1]+A_PUP[1]*A_CROP[2],FACE_IRIS,A_PR*A_CROP[2]);
for(var n in pending) upload(n,pending[n]);
resize();
}
// Stills load behind stage 1. The line at the bottom fills by bytes received.
var STILLS=[['A','img/pupil.webp'],['B','img/face.webp'],['C','img/earth.webp']];
var got=[0,0,0], tot=[0,0,0], done=0, loaderTimer=setTimeout(function(){ load.classList.add('on'); }, 250);
function showBar(){
var g=0,t=0; for(var i=0;i<3;i++){ g+=got[i]; t+=tot[i]||250000; }
bar.style.transform='scaleX('+Math.min(1,(g/t)*.95+done/3*.05).toFixed(3)+')';
}
var pending={};
function upload(name,bmp){
if(!gl || !tex[name]){ pending[name]=bmp; return; }
var unit=['A','B','C'].indexOf(name);
gl.activeTexture(gl.TEXTURE0+unit); gl.bindTexture(gl.TEXTURE_2D,tex[name]);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,bmp);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
}
function finished(){
done++; showBar();
if(done===3){ clearTimeout(loaderTimer); setTimeout(function(){ load.classList.remove('on'); }, 350); }
}
function fetchStill(i){
var x=new XMLHttpRequest(); x.open('GET',STILLS[i][1]); x.responseType='blob';
x.onprogress=function(e){ got[i]=e.loaded; if(e.lengthComputable) tot[i]=e.total; showBar(); };
x.onload=function(){
got[i]=tot[i]=x.response.size;
var im=new Image(); im.decoding='async';
im.onload=function(){ upload(STILLS[i][0],im); finished(); };
im.onerror=finished;
im.src=URL.createObjectURL(x.response);
};
x.onerror=finished;
x.send();
}
function local(cam,o){ return [(cam.c[0]-o.c[0])/o.s+.5, .5-(cam.c[1]-o.c[1])/o.s, cam.v/o.s]; }
var last=performance.now(), t0=last, slow=0, frames=0;
function frame(now){
var dt=Math.min(.1,(now-last)/1000); last=now;
if(gl && (canvas.clientWidth!==boxW || canvas.clientHeight!==boxH)) resize(); // ASPECT FIX: catch any box change the events missed
cur += (target-cur)*(1-Math.exp(-dt*(reduce||touch?40:6.5))); // SCROLL FIX: no smoothing lag on touch
if(Math.abs(target-cur)<1e-5) cur=target;
mouse[0]+=(mt[0]-mouse[0])*(1-Math.exp(-dt*2)); mouse[1]+=(mt[1]-mouse[1])*(1-Math.exp(-dt*2));
textAt(cur);
if(gl && !document.hidden){
var cam=camera(cur);
cam.c=[cam.c[0]+mouse[0]*.012*cam.v, cam.c[1]+mouse[1]*.012*cam.v];
var v=cam.v;
gl.uniform2f(U.uRes,canvas.width,canvas.height);
gl.uniform1f(U.uTime,reduce?0:(now-t0)/1000);
gl.uniform1f(U.uOct,innerWidth<700?3:5);
gl.uniform2f(U.uMouse,mouse[0],mouse[1]);
gl.uniform3f(U.uQ,cam.c[0]/NEB,cam.c[1]/NEB,v/NEB);
gl.uniform3f(U.uP,(cam.c[0]-W.P.c[0])/W.P.r,(cam.c[1]-W.P.c[1])/W.P.r,v/W.P.r);
gl.uniform3fv(U.uA,local(cam,W.A)); gl.uniform3fv(U.uB,local(cam,W.B)); gl.uniform3fv(U.uC,local(cam,W.C));
// crossfades run on the size of each still on screen, so each handoff happens at matched scale
gl.uniform1f(U.uCosA,ss(.45,1.2,W.P.r/v));
gl.uniform1f(U.uAA,ss(1.2,3,W.A.s/v));
gl.uniform1f(U.uBA,ss(.6,1.5,W.B.s/v));
gl.uniform1f(U.uDim,ss(.8,2,W.B.s/v));
var e=.06/v; // Earth radius on screen, in half screen heights
gl.uniform1f(U.uEA,ss(.05,.11,e)); // Earth fades into the core point as it shrinks
gl.uniform1f(U.uQA,1-ss(.3,.6,e)); // the nebula comes up around Earth once we leave the Earth stage
gl.uniform1f(U.uCore,1-ss(.05,.13,e)); // the core lights up to its opening strength as Earth fades into it
gl.drawArrays(gl.TRIANGLES,0,3);
if(!shown){ shown=true; canvas.classList.add('on'); }
frames++;
if(frames>20){ if(dt>.03) slow++; else slow=Math.max(0,slow-1);
if(slow>40 && scale>.55){ scale*=.8; slow=0; resize(); } }
}
requestAnimationFrame(frame);
}
for(var i=0;i<3;i++) fetchStill(i);
requestAnimationFrame(function(){ setTimeout(function(){ init(); requestAnimationFrame(frame); }, 0); });
})();
</script>
</body>
</html>
|