类型:Site模板 · 收录:2026-11-05 01:11:51 · 源码:会员可见
# Infinite Gallery — Aurelis (exact recreation prompt)
Give me a single-file, full-viewport page called **Aurelis**: an endless dark canvas of live website preview videos that you can drag in any direction forever. As the canvas moves, every card smears into a coloured motion streak, then snaps sharp as it slows. Clicking a card grows that same playing video to full screen, and the title, stats and a small product panel come in over it.
Plain HTML, CSS and one vanilla script with raw WebGL2, in one file, with no libraries. The only external resources are the Google Fonts and the twelve video URLs below. Reproduce every value, string and timing exactly. The CSS, markup and script are given verbatim, so copy them without changes.
Five rules define it:
1. **One repeating tile.** The whole world is one 2800 × 2560 tile drawn to a 2D canvas and uploaded as a `REPEAT` texture. Scrolling just offsets the texture, which is why the canvas is truly infinite.
2. **Live video in the tile.** Each visible card's video is painted into its exact rectangle in that texture with `texSubImage2D`, every second frame. Cards stay in the same place in the tile.
3. **Everything visual runs in one fragment shader.** The motion blur, colour fringing and preview opening all happen on the GPU, so nothing is animated in the page layer except text.
4. **Cards keep their media's shape.** A card's height is its width divided by that video's own aspect ratio, so no preview is ever cropped.
5. **Only what is on screen plays.** Videos load lazily on first sight, and any video with no visible card pauses.
---
## 1. Page basics
- Title: `Aurelis — Infinite Prompts`
- Meta description: `An endless drag-anywhere gallery of live website previews where every card streaks into colour as it moves.`
- Fonts: `https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Outfit:wght@400;500&display=swap`, with preconnects to fonts.googleapis.com and fonts.gstatic.com (crossorigin). The supplied standalone file embeds the latin woff2 files as base64 instead.
- Outfit for the wordmark, card titles, preview title and stat numbers. Inter for everything else.
- Colours: background `#09090b`, ink `#f5f4f0`, mid `#a3a3a8`, green `#86ef6f`, new badge `#8b5cf6`, grid lines `rgba(255,255,255,.075)`, card placeholder `#111114`.
## 2. Videos (use these exact URLs)
All are hosted with `Access-Control-Allow-Origin: *`. Each is created with `crossOrigin="anonymous"`, muted, looping, inline and `preload="auto"`, without being added to the page.
1. Shelf Hero (Portfolio, 1440 × 960): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/vasari-vhs-hero.mp4`
2. Facet Glass (3D, 1440 × 960): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/facet-glass-hero.mp4`
3. Saffron Field (Interactive, 1920 × 1410): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/saffron-3d-webgl-landing.mp4`
4. Nomi AI Agent (AI, 1440 × 900): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/nomi-hero.mp4`
5. Bookshop (Portfolio, 1600 × 900): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/folio-books-hero.mp4`
6. Meadowbell (Kids Show, 1440 × 1280): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/meadowbell-hero.mp4`
7. Living Portrait (Portfolio, 1600 × 900): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/living-portrait-hero.mp4`
8. Robot Jellyfish (3D, 1600 × 900): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/medusa-jellyfish-hero.mp4`
9. Space Voyage (Creative, 1920 × 1416): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/space-voyage-video.mp4`
10. Honey Bee (Creative, 1920 × 1414): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/honey-bee-melli-landing.mp4`
11. Roadster 3D (3D, 1920 × 1412): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/roadster-3d-car-landing.mp4`
12. Obsidian (Interactive, 1468 × 1080): `https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/a/obsisidianArea.mp4`
## 3. The world
- 25 card slots in 5 columns and 5 rows. Column pitch 560, row pitch 470. Each slot starts at `x = i * 560 + 64 + (j % 2) * 26`, `y = j * 470 + YOFF[i] + 40` with `YOFF = [0,130,60,190,90]`.
- Widths cycle through `[392,426,372,410,384]` by `(i + j * 2) % 5`.
- Video order is the `SLOTS` list in the script, chosen so that the two copies of a video sit as far apart as possible.
- Below each card: the title on the left in Outfit 500 at 20px, and a green dot with the category on the right in Inter 12.5px.
- A hairline frame surrounds each card, plus small 62px grid cells placed from a seeded random generator (seed 4021). The texture scale is `min(1.6, dpr * 1.1)`.
## 4. Movement
- World zoom is `max(.62, innerWidth / 1600)`.
- Drag moves the world 1:1. On release it carries on with momentum, multiplied by `.945` per 60fps frame. Holding still during a drag decays the speed by `.82` per frame, so the blur clears.
- The wheel adds `delta * .09` to the speed and the arrow keys add 26.
- After about 2.5 seconds idle, the world eases into a slow drift of `(.28, .16)` px per frame.
- On load it starts with a fling (`velocity 47, -31`) that settles by itself.
## 5. The streak (fragment shader)
- Blur vector is `velocity * 2.4`, clamped to ±155 per axis and eased toward its target by `1 - .72^dt`.
- Below a length of 1 the shader takes one plain sample. Otherwise it takes 30 samples along the vector, each offset by interleaved gradient noise and a small sideways jitter of `len * .11`.
- Each sample is weighted by a spectral curve along the streak, red at one end, green in the middle and blue at the other, mixed in by `clamp(len / 70, 0, 1)`. This gives the coloured fringe on every card edge.
## 6. Interface
- **Top bar,** 76px, three columns: a 40px round menu button and `Platform`, `Company`, `Products`, `FAQs` on the left; the lowercase wordmark `aurelis` in Outfit 400 at 30px in the centre; search, a gradient avatar, `28 · Credits`, a green `Get Credits →` pill and a moon icon on the right.
- **Dock,** centred 22px from the bottom, 54px tall, glass pill with `rgba(22,22,25,.78)` and an 18px backdrop blur: `Collections` (active), `3D Objects`, `Animation`, `Characters`, `Vehicles`, then a round green plus.
- **Cursor:** a 72px ring following the mouse, reading `Drag` on the canvas, `Open` over a card and `Close` while a preview is open. It shrinks to 52px while pressed.
## 7. Opening a preview
A press that moves under 7px in under 400ms counts as a click on the card under the cursor.
- **Motion:** 840ms with `1 - (1 - x)^4`, so it moves immediately. The shader draws a rounded rectangle that interpolates from the card's rect to the full screen, with the video inside always cover-fitted to that rectangle. The video texture is updated every second frame while it grows, so playback never stops.
- **Morph:** it starts on the card's own pixels from the tile and mixes to the full-size video over the first 2–14% of the eased progress.
- **Behind it:** the gallery darkens by 80% and scales up 7% around the card.
- **Fringe:** the video's red and blue channels split outward while it grows and join again as it settles.
- **Shade:** a top and bottom darkening fades in for the text.
- **Text:** the two title lines rise from behind a mask from 190ms, 70ms apart, over 760ms. The stats rise from 300ms, the panel slides in from 380ms, and the dock drops away. The stats count from 90% to their final value over 1300ms.
- **Closing:** click anywhere or press Esc. It reverses over 520ms with a cubic ease in and out, and the gallery continues from the same spot.
The preview shows the title, `<category> Prompt`, `Collection Views`, `Purchase` with a `New` badge, `Recommended`, and a panel with the category, a price and a QR-style pattern. These figures are placeholder content.
## 8. Phones
Below 980px the nav links and credits hide. Below 640px the bar is 64px tall, the wordmark is 25px, the dock spans the width and scrolls sideways with the plus button stuck to its right edge, and the preview title and stats stack at the bottom. The ring cursor is removed on touch screens.
## 9. Reduced motion
With `prefers-reduced-motion: reduce` there is no fling, drift or blur, the videos stay on their first frame, and the preview opens instantly.
## 10. CSS (use verbatim)
```css
:root{--bg:#09090b;--ink:#f5f4f0;--mid:#a3a3a8;--green:#86ef6f}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:var(--bg);overscroll-behavior:none}
body{font-family:Inter,system-ui,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
#stage{position:fixed;inset:0;touch-action:none;cursor:none;user-select:none;-webkit-user-select:none}
#gl{position:absolute;inset:0;width:100%;height:100%;display:block}
.top{position:absolute;left:0;right:0;top:0;height:76px;padding:0 28px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;pointer-events:none}
.top>*{pointer-events:auto}
.left,.right{display:flex;align-items:center;gap:26px}
.right{justify-content:flex-end;gap:18px}
.burger{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:var(--ink);color:#0b0b0d}
.burger svg{width:18px;height:18px}
.left a{font-size:14px;color:#dcdbd7;display:flex;align-items:center;gap:5px;transition:color .2s}
.left a:hover{color:#fff}
.left a svg{width:9px;height:9px}
.mark{font:400 30px/1 Outfit,sans-serif;letter-spacing:-.02em}
.icon{display:grid;place-items:center;width:28px;height:28px}
.icon svg{width:18px;height:18px}
.avatar{width:30px;height:30px;border-radius:50%;background:radial-gradient(circle at 34% 30%,#f7e6c4,#c98b4b 58%,#4a2b18)}
.credits{font-size:14px;color:var(--mid);white-space:nowrap}
.credits b{font-weight:500;color:var(--ink)}
.cta{display:flex;align-items:center;gap:8px;height:40px;padding:0 20px;border-radius:999px;background:var(--green);color:#0b1408;font-size:14px;font-weight:500;white-space:nowrap;transition:transform .2s}
.cta:hover{transform:translateY(-1px)}
.cta svg{width:14px;height:14px}
.dock{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);display:flex;align-items:center;gap:4px;height:54px;padding:0 7px;border-radius:999px;background:rgba(22,22,25,.78);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.07)}
.dock button{display:flex;align-items:center;gap:7px;height:40px;padding:0 15px;border-radius:999px;font-size:14px;color:#c4c3c0;white-space:nowrap;transition:color .2s,background .2s}
.dock button:hover{color:#fff}
.dock button.on{background:rgba(255,255,255,.1);color:#fff}
.dock button svg{width:15px;height:15px}
.dock .plus{width:40px;padding:0;justify-content:center;background:var(--green);color:#0b1408;margin-left:6px}
.dock .plus:hover{color:#0b1408}
.top{z-index:3}
.dock{z-index:3}
#pv{position:absolute;inset:0;z-index:2;overflow:hidden;pointer-events:none}
#pv[hidden]{display:none}
.pv-title{position:absolute;left:44px;bottom:44px;font:400 clamp(30px,3.5vw,58px)/1.1 Outfit,sans-serif;letter-spacing:-.025em}
.pv-title span{display:block;overflow:hidden;padding-bottom:.08em}
.pv-title i{display:block;font-style:normal;will-change:transform}
.pv-stats{position:absolute;right:44px;bottom:50px;display:flex;gap:clamp(26px,3.6vw,58px)}
.pv-stats b{display:block;font:400 clamp(24px,2.5vw,40px)/1 Outfit,sans-serif;letter-spacing:-.02em}
.pv-stats small{display:flex;align-items:center;gap:6px;margin-top:9px;font-size:12px;color:var(--mid)}
.pv-stats em{font-style:normal;font-size:10px;line-height:1;padding:3px 6px;border-radius:99px;background:#8b5cf6;color:#fff}
.pv-panel{position:absolute;right:28px;top:96px;width:150px;padding:13px;border-radius:12px;background:rgba(14,14,17,.56);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.1)}
.pv-panel p{display:flex;justify-content:space-between;font-size:12px;color:#cfcecb}
.pv-panel strong{display:block;margin:8px 0 10px;font:500 21px/1 Outfit,sans-serif;color:var(--green)}
.pv-panel svg{display:block;width:100%;border-radius:6px;background:#f5f4f0}
#cur{z-index:4;position:fixed;left:0;top:0;width:72px;height:72px;margin:-36px 0 0 -36px;border-radius:50%;border:1px solid rgba(255,255,255,.72);display:grid;place-items:center;font-size:12px;color:#fff;pointer-events:none;transition:width .25s,height .25s,margin .25s,background .25s,opacity .25s;will-change:transform;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
#cur.down{width:52px;height:52px;margin:-26px 0 0 -26px;background:rgba(255,255,255,.12)}
#cur.off{opacity:0}
@media (hover:none){#cur{display:none}#stage{cursor:auto}}
@media (max-width:980px){.left a,.credits{display:none}}
@media (max-width:640px){
.pv-title{left:18px;bottom:112px}
.pv-stats{left:18px;right:18px;bottom:40px;justify-content:space-between}
.pv-panel{display:none}
.top{height:64px;padding:0 14px}
.mark{font-size:25px}
.avatar,.icon{display:none}
.cta{height:36px;padding:0 13px;font-size:13px}
.burger{width:36px;height:36px}
.dock{left:12px;right:12px;bottom:14px;transform:none;overflow-x:auto;scrollbar-width:none}
.dock::-webkit-scrollbar{display:none}
.dock .plus{flex:none;position:sticky;right:0}
}
```
## 11. Markup (use verbatim, as the contents of body before the script)
```html
<main id="stage" aria-label="Infinite gallery. Drag or use the arrow keys to move.">
<canvas id="gl" aria-hidden="true"></canvas>
<section id="pv" hidden aria-live="polite">
<h2 class="pv-title"><span><i id="pvA"></i></span><span><i id="pvB"></i></span></h2>
<div class="pv-stats">
<div><b id="pv1"></b><small>Collection Views</small></div>
<div><b id="pv2"></b><small>Purchase <em>New</em></small></div>
<div><b id="pv3"></b><small>Recommended</small></div>
</div>
<aside class="pv-panel"><p><span id="pvC"></span><span aria-hidden="true">×</span></p><strong id="pvP"></strong><svg id="pvQ" viewBox="0 0 13 13" aria-hidden="true"></svg></aside>
</section>
<header class="top">
<nav class="left" aria-label="Main">
<button class="burger" aria-label="Menu"><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 5.5h12M3 9h12M3 12.5h12"/></svg></button>
<a href="#">Platform <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4"><path d="m2 3.5 3 3 3-3"/></svg></a>
<a href="#">Company <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4"><path d="m2 3.5 3 3 3-3"/></svg></a>
<a href="#">Products <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4"><path d="m2 3.5 3 3 3-3"/></svg></a>
<a href="#">FAQs</a>
</nav>
<a class="mark" href="#">aurelis</a>
<div class="right">
<button class="icon" aria-label="Search"><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="5.2"/><path d="m12 12 3.5 3.5" stroke-linecap="round"/></svg></button>
<span class="avatar" aria-hidden="true"></span>
<span class="credits"><b>28</b> · Credits</span>
<a class="cta" href="#">Get Credits <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 7h9M7.5 3l4 4-4 4"/></svg></a>
<button class="icon" aria-label="Theme"><svg viewBox="0 0 18 18" fill="currentColor"><path d="M15.5 11.2A7 7 0 0 1 6.8 2.5a7 7 0 1 0 8.7 8.7Z"/></svg></button>
</div>
</header>
<div class="dock" role="toolbar" aria-label="Filters">
<button class="on"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1 1.5 4.5 8 8l6.5-3.5Zm-6.5 6.3L8 10.8l6.5-3.5v1.7L8 12.5 1.5 9Zm0 3.2L8 14l6.5-3.5V12L8 15.5 1.5 12Z"/></svg>Collections</button>
<button><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"><path d="M8 1.5 14 5v6l-6 3.5L2 11V5Zm0 6.5L2 5m6 3 6-3M8 8v6.5"/></svg>3D Objects</button>
<button><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"><path d="M4 2.5v11l9-5.5Z"/></svg>Animation</button>
<button><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><circle cx="8" cy="5" r="3"/><path d="M2.5 14.5c.8-3.2 3-4.5 5.5-4.5s4.7 1.3 5.5 4.5"/></svg>Characters</button>
<button><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"><path d="M2 11V8l2-4h8l2 4v3Zm2 0v2m8-2v2M2 8h12"/></svg>Vehicles</button>
<button class="plus" aria-label="Add"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>
</div>
<div id="cur" class="off" aria-hidden="true">Drag</div>
</main>
```
## 12. Script (use verbatim)
```js
const CARDS=[
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/vasari-vhs-hero.mp4','Shelf Hero','Portfolio',1440/960],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/facet-glass-hero.mp4','Facet Glass','3D',1440/960],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/saffron-3d-webgl-landing.mp4','Saffron Field','Interactive',1920/1410],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/nomi-hero.mp4','Nomi AI Agent','AI',1440/900],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/folio-books-hero.mp4','Bookshop','Portfolio',1600/900],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/meadowbell-hero.mp4','Meadowbell','Kids Show',1440/1280],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/living-portrait-hero.mp4','Living Portrait','Portfolio',1600/900],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/aaaf/medusa-jellyfish-hero.mp4','Robot Jellyfish','3D',1600/900],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/space-voyage-video.mp4','Space Voyage','Creative',1920/1416],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/honey-bee-melli-landing.mp4','Honey Bee','Creative',1920/1414],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/designs/roadster-3d-car-landing.mp4','Roadster 3D','3D',1920/1412],
['https://pub-86dc5b5484314368ac5436a674b0d919.r2.dev/a/obsisidianArea.mp4','Obsidian','Interactive',1468/1080]
];
const TW=2800,TH=2560,COLS=5,ROWS=5,PX=560,PY=470;
const YOFF=[0,130,60,190,90],WID=[392,426,372,410,384];
const SLOTS=[4,8,11,7,6,10,9,1,3,2,11,2,0,9,5,0,6,7,10,4,1,3,5,0,8];
const span=(x,a,b)=>Math.min(1,Math.max(0,(x-a)/(b-a)));
const LAYOUT=[];
function seeded(s){return()=>(s=(s*16807)%2147483647)/2147483647}
function rounded(g,x,y,w,h,r){g.beginPath();g.roundRect(x,y,w,h,r)}
function drawLabel(g,c,x,y,w,h){
const [,title,cat]=CARDS[c];
g.fillStyle='#111114';rounded(g,x,y,w,h,5);g.fill();
g.textBaseline='alphabetic';
g.fillStyle='#f5f4f0';g.font='500 20px Outfit';g.letterSpacing='-.2px';g.fillText(title,x+2,y+h+30);g.letterSpacing='0px';
const cw=(g.font='400 12.5px Inter',g.measureText(cat).width);
g.fillStyle='#b9b8b4';g.fillText(cat,x+w-cw-2,y+h+29);
g.fillStyle='#86ef6f';g.beginPath();g.arc(x+w-cw-13,y+h+25,3.5,0,7);g.fill();
}
function drawTile(scale){
const c=document.createElement('canvas');c.width=TW*scale|0;c.height=TH*scale|0;
const g=c.getContext('2d');g.scale(scale,scale);g.fillStyle='#09090b';g.fillRect(0,0,TW,TH);
const r=seeded(4021),rects=[],cards=LAYOUT;
for(let i=0;i<COLS;i++)for(let j=0;j<ROWS;j++){
const w=WID[(i+j*2)%5],h=Math.round(w/CARDS[SLOTS[i*5+j]][3]),x=i*PX+64+(j%2)*26,y=j*PY+YOFF[i]+40;
cards.push([SLOTS[i*5+j],x,y,w,h]);
rects.push([x-40,y-40,w+80,h+110]);
if(r()>.3)rects.push([x+w+40,y+40+r()*120,62,62]);
if(r()>.45)rects.push([x-40,y+h+70,124,62]);
}
for(let n=0;n<170;n++){const s=62,x=Math.floor(r()*TW/s)*s,y=Math.floor(r()*TH/s)*s;rects.push([x,y,s*(1+Math.floor(r()*2)),s])}
const each=fn=>{for(const dx of[-TW,0,TW])for(const dy of[-TH,0,TH]){g.save();g.translate(dx,dy);fn();g.restore()}};
g.strokeStyle='rgba(255,255,255,.075)';g.lineWidth=1;
each(()=>rects.forEach(q=>g.strokeRect(q[0]+.5,q[1]+.5,q[2],q[3])));
cards.forEach(q=>drawLabel(g,q[0],q[1],q[2],q[3],q[4]));
return c;
}
let tileScale=1,tick=0;
const vids=CARDS.map(()=>null),paint=document.createElement('canvas'),pg=paint.getContext('2d',{alpha:false});
function video(c){
if(vids[c])return vids[c];
const v=document.createElement('video');
v.crossOrigin='anonymous';v.muted=true;v.loop=true;v.playsInline=true;v.preload='auto';v.src=CARDS[c][0];
return vids[c]=v;
}
function seen(){
const out=[],vw=innerWidth/zoom,vh=innerHeight/zoom;
for(const q of LAYOUT){
let rx=((q[1]-pos.x)%TW+TW)%TW,ry=((q[2]-pos.y)%TH+TH)%TH;
if(rx>TW-q[3])rx-=TW;if(ry>TH-q[4])ry-=TH;
if(rx<vw&&ry<vh)out.push(q);
}
return out;
}
function stamp(q){
const v=video(q[0]);if(v.readyState<2)return;
const w=Math.round(q[3]*tileScale),h=Math.round(q[4]*tileScale);
if(paint.width!==w||paint.height!==h){paint.width=w;paint.height=h}
const k=Math.max(w/v.videoWidth,h/v.videoHeight),dw=v.videoWidth*k,dh=v.videoHeight*k;
pg.fillStyle='#09090b';pg.fillRect(0,0,w,h);
pg.save();rounded(pg,0,0,w,h,5*tileScale);pg.clip();pg.drawImage(v,(w-dw)/2,(h-dh)/2,dw,dh);pg.restore();
gl.texSubImage2D(gl.TEXTURE_2D,0,Math.round(q[1]*tileScale),Math.round(q[2]*tileScale),gl.RGBA,gl.UNSIGNED_BYTE,paint);
}
function pump(){
tick++;
if(pv.open&&pv.v){
if(pv.v.readyState>=2&&tick%2===0){gl.activeTexture(gl.TEXTURE1);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,pv.v);gl.activeTexture(gl.TEXTURE0)}
if(pv.dir===0)return;
}
const list=seen();
if(tick%10===1){
const want=new Set(list.map(q=>q[0]));
if(pv.v)want.add(pv.hit?pv.hit.c:-1);
CARDS.forEach((_,c)=>{
if(want.has(c)){const v=video(c);if(v.paused&&!calm)v.play().catch(()=>{})}
else if(vids[c]&&!vids[c].paused)vids[c].pause();
});
}
if(tick%2===0)list.forEach(stamp);
}
const VS=`#version 300 es
in vec2 a;void main(){gl_Position=vec4(a,0.,1.);}`;
const FS=`#version 300 es
precision highp float;
uniform sampler2D T,W;uniform vec2 R,P,V,S,CW,CH,GC;uniform vec4 Q,CELL;uniform float Z,O,M,F;out vec4 o;
const int N=30;
float ign(vec2 p){return fract(52.9829189*fract(dot(p,vec2(.06711056,.00583715))));}
vec3 gallery(vec2 px){
float zz=Z*(1.+.07*O);
vec2 w=GC/Z+(px-GC)/zz+P;
float len=length(V);
if(len<1.)return texture(T,w/S).rgb;
vec2 d=V/len,n=vec2(-d.y,d.x);
float j=ign(gl_FragCoord.xy);
vec3 col=vec3(0.),sum=vec3(0.);
float fringe=clamp(len/70.,0.,1.);
for(int i=0;i<N;i++){
float t=(float(i)+j)/float(N)-.5;
vec3 k=mix(vec3(1.),vec3(smoothstep(-.28,.34,t),1.-smoothstep(.12,.5,abs(t))*.82,1.-smoothstep(-.34,.28,t)),fringe);
float side=(fract(j*7.3+float(i)*.618)-.5)*len*.11;
col+=texture(T,(w+V*t+n*side)/S).rgb*k;sum+=k;
}
return col/sum;
}
vec3 wide(vec2 uv,vec2 lo,vec2 hi){return texture(W,clamp(uv,lo,hi)).rgb;}
void main(){
vec2 px=vec2(gl_FragCoord.x,R.y-gl_FragCoord.y);
vec3 g=gallery(px);
if(O<=.0005){o=vec4(g,1.);return;}
g*=1.-.8*smoothstep(0.,.55,O);
vec2 qc=Q.xy+Q.zw*.5,rel=px-qc,cellPx=CELL.zw*vec2(textureSize(W,0));
float rad=(1.-O)*6.*Z;vec2 qd=abs(rel)-Q.zw*.5+rad;float sd=length(max(qd,0.))+min(max(qd.x,qd.y),0.)-rad;
float a=1.-smoothstep(-.8,.8,sd);
if(a<=0.){o=vec4(g,1.);return;}
float s=max(Q.z/cellPx.x,Q.w/cellPx.y);
vec2 uv=rel/s/vec2(textureSize(W,0));
vec2 c=CELL.xy+CELL.zw*.5,lo=CELL.xy,hi=CELL.xy+CELL.zw;
vec2 sp=rel/R.y*F*.018;
vec3 im=vec3(wide(c+uv+sp,lo,hi).r,wide(c+uv,lo,hi).g,wide(c+uv-sp,lo,hi).b);
vec3 card=texture(T,(CW+rel/max(Q.z/CH.x,Q.w/CH.y))/S).rgb;
vec3 col=mix(card,im,M);
float y=px.y/R.y,top=1.-.62*(1.-smoothstep(0.,.18,y)),bot=1.-.9*smoothstep(.56,1.,y);
col*=mix(1.,top*bot,smoothstep(.45,1.,O));
o=vec4(mix(g,col,a),1.);
}`;
const stage=document.getElementById('stage'),cv=document.getElementById('gl'),cur=document.getElementById('cur');
const calm=matchMedia('(prefers-reduced-motion:reduce)').matches;
const gl=cv.getContext('webgl2',{antialias:false,alpha:false,preserveDrawingBuffer:false});
let U={},zoom=1,dpr=1;
const pos={x:880,y:560},vel={x:0,y:0},blur={x:0,y:0};
let drag=null,last=0,prev=0,idle=0,live=true;
function resize(){
dpr=Math.min(devicePixelRatio||1,2);
cv.width=innerWidth*dpr|0;cv.height=innerHeight*dpr|0;
zoom=Math.max(.62,innerWidth/1600);
if(gl){gl.viewport(0,0,cv.width,cv.height);gl.uniform2f(U.R,cv.width,cv.height);gl.uniform1f(U.Z,zoom*dpr)}
}
function cardAt(cx,cy){
const wx=pos.x+cx/zoom,wy=pos.y+cy/zoom;
for(const q of LAYOUT){
const nx=((wx-q[1])%TW+TW)%TW,ny=((wy-q[2])%TH+TH)%TH;
if(nx<q[3]&&ny<q[4])return{c:q[0],x:cx-nx*zoom,y:cy-ny*zoom,w:q[3]*zoom,h:q[4]*zoom};
}
return null;
}
const pv={el:document.getElementById('pv'),open:false,busy:false,anims:[],count:null,to:[0,0,0],p:0,dir:0,hit:null,v:null};
const pvIn=pv.el.querySelectorAll('.pv-title i');
const $=id=>document.getElementById(id),OUT='cubic-bezier(.16,1,.3,1)',OPEN=calm?1:840,SHUT=calm?1:520;
const expo=x=>1-Math.pow(1-x,4),inOut=x=>x<.5?4*x*x*x:1-Math.pow(-2*x+2,3)/2,lerp=(a,b,k)=>a+(b-a)*k;
function countUp(){
if(!pv.count)return;
const p=pv.count.effect.getComputedTiming().progress||0;
[$('pv1'),$('pv2'),$('pv3')].forEach((el,i)=>{const v=pv.to[i]*(.9+.1*p);el.textContent=i==1?(v/10).toFixed(1)+'k':Math.round(v)+'k'});
}
function qr(seed){
const r=seeded(seed*97+13);let s='';
const box=(x,y)=>`<path d="M${x} ${y}h3v3h-3zM${x+.7} ${y+.7}v1.6h1.6v-1.6z" fill-rule="evenodd"/>`;
for(let y=1;y<12;y++)for(let x=1;x<12;x++){if((x<5&&y<5)||(x>7&&y<5)||(x<5&&y>7))continue;if(r()>.5)s+=`<rect x="${x}" y="${y}" width="1" height="1"/>`}
$('pvQ').innerHTML=`<g fill="#0b0b0d">${box(1,1)+box(9,1)+box(1,9)+s}</g>`;
}
function paintPv(){
const h=pv.hit;if(!h)return;
const e=pv.dir>=0?expo(pv.p):inOut(pv.p),W=cv.width,H=cv.height,k=dpr;
const x=lerp(h.x*k,0,e),y=lerp(h.y*k,0,e),w=lerp(h.w*k,W,e),o=Math.min(1,e);
gl.uniform4f(U.Q,x,y,w,lerp(h.h*k,H,e));
gl.uniform1f(U.O,o);
gl.uniform1f(U.M,span(e,.02,.14));
gl.uniform1f(U.F,(1-e)*Math.min(1,e*9));
countUp();
}
function tickPv(ms){
if(pv.dir>0){pv.p=Math.min(1,pv.p+ms/OPEN);if(pv.p>=1){pv.dir=0;pv.busy=false}}
else if(pv.dir<0){
pv.p=Math.max(0,pv.p-ms/SHUT);
if(pv.p<=0){pv.anims.forEach(x=>x.cancel());pv.anims=[];pv.count=null;pv.v=null;pv.el.hidden=true;pv.open=pv.busy=false;pv.hit=null;pv.dir=0;idle=0;gl.uniform1f(U.O,0);return}
}
paintPv();
}
function openPreview(hit){
const [,title,cat]=CARDS[hit.c];
pv.v=video(hit.c);pv.open=pv.busy=true;pv.hit=hit;pv.p=0;pv.dir=1;vel.x=vel.y=blur.x=blur.y=0;cur.textContent='Close';
gl.uniform2f(U.CW,((pos.x%TW)+TW)%TW+(hit.x+hit.w/2)/zoom,((pos.y%TH)+TH)%TH+(hit.y+hit.h/2)/zoom);
gl.uniform2f(U.CH,hit.w/zoom,hit.h/zoom);
gl.uniform2f(U.GC,(hit.x+hit.w/2)*dpr,(hit.y+hit.h/2)*dpr);
gl.uniform4f(U.CELL,0,0,1,1);
if(pv.v.readyState>=2){gl.activeTexture(gl.TEXTURE1);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,pv.v);gl.activeTexture(gl.TEXTURE0)}
$('pvA').textContent=title;$('pvB').textContent=cat+' Prompt';$('pvC').textContent=cat;$('pvP').textContent='$'+(19+hit.c*2)+'.99';
pv.to=[64+hit.c*7,18+hit.c,26+hit.c*2];qr(hit.c);
pv.el.hidden=false;
pv.anims=[];
const T=(el,kf,o)=>{const an=el.animate(kf,{fill:'both',easing:OUT,...o,duration:calm?1:o.duration,delay:calm?0:o.delay||0});pv.anims.push(an);return an};
pvIn.forEach((el,i)=>T(el,[{transform:'translateY(115%)'},{transform:'none'}],{duration:760,delay:190+i*70}));
pv.el.querySelectorAll('.pv-stats>div').forEach((el,i)=>T(el,[{transform:'translateY(22px)',opacity:0},{transform:'none',opacity:1}],{duration:620,delay:300+i*70}));
T(pv.el.querySelector('.pv-panel'),[{transform:'translateX(22px)',opacity:0},{transform:'none',opacity:1}],{duration:620,delay:380});
T(document.querySelector('.dock'),[{translate:'0 0',opacity:1},{translate:'0 90px',opacity:0}],{duration:380});
pv.count=T(pv.el.querySelector('.pv-stats'),[{translate:'0 0'},{translate:'0 0'}],{duration:1300,delay:300,easing:'cubic-bezier(.2,.7,.2,1)'});
paintPv();
}
function closePreview(){
pv.busy=true;pv.dir=-1;cur.textContent='Drag';
pv.anims.forEach(x=>{x.currentTime=Math.min(x.currentTime,420);x.playbackRate=calm?1:-2;x.play()});
}
function render(){
gl.uniform2f(U.P,((pos.x%TW)+TW)%TW,((pos.y%TH)+TH)%TH);
gl.uniform2f(U.V,blur.x,blur.y);
gl.drawArrays(gl.TRIANGLES,0,3);
}
function frame(t){
if(!live)return;
const dt=Math.min(2.5,last?(t-last)/16.67:1);last=t;
pump();
if(pv.open){tickPv(Math.min(40,t-prev));render();prev=t;requestAnimationFrame(frame);return}
prev=t;
if(!drag){
const fr=Math.pow(.945,dt);vel.x*=fr;vel.y*=fr;
pos.x+=vel.x*dt;pos.y+=vel.y*dt;
idle+=dt;
if(!calm&&idle>150){const k=span(idle,150,240);pos.x+=.28*k*dt;pos.y+=.16*k*dt}
}
else{const fr=Math.pow(.82,dt);vel.x*=fr;vel.y*=fr}
const tx=calm?0:Math.max(-155,Math.min(155,vel.x*2.4)),ty=calm?0:Math.max(-155,Math.min(155,vel.y*2.4));
const b=1-Math.pow(.72,dt);blur.x+=(tx-blur.x)*b;blur.y+=(ty-blur.y)*b;
render();requestAnimationFrame(frame);
}
stage.addEventListener('pointerdown',e=>{
if(e.target.closest('a,button'))return;
if(pv.open){if(!pv.busy)closePreview();return}
drag={x:e.clientX,y:e.clientY,t:performance.now(),sx:e.clientX,sy:e.clientY,st:performance.now()};vel.x=vel.y=0;idle=0;
stage.setPointerCapture(e.pointerId);cur.classList.add('down');
});
stage.addEventListener('pointermove',e=>{
cur.style.transform=`translate(${e.clientX}px,${e.clientY}px)`;
cur.classList.toggle('off',!!e.target.closest('a,button'));
const word=pv.open?'Close':!drag&&cardAt(e.clientX,e.clientY)?'Open':'Drag';if(cur.textContent!==word)cur.textContent=word;
if(!drag)return;
const dx=(e.clientX-drag.x)/zoom,dy=(e.clientY-drag.y)/zoom,now=performance.now(),dt=Math.max(1,now-drag.t)/16.67;
pos.x-=dx;pos.y-=dy;
vel.x+=(-dx/dt-vel.x)*.5;vel.y+=(-dy/dt-vel.y)*.5;
Object.assign(drag,{x:e.clientX,y:e.clientY,t:now});idle=0;
blur.x+=(vel.x*2.4-blur.x)*.35;blur.y+=(vel.y*2.4-blur.y)*.35;
});
const up=e=>{
if(!drag)return;
const tap=Math.hypot(e.clientX-drag.sx,e.clientY-drag.sy)<7&&performance.now()-drag.st<400;
if(performance.now()-drag.t>90)vel.x=vel.y=0;
drag=null;cur.classList.remove('down');
if(tap&&e.type==='pointerup'){const hit=cardAt(e.clientX,e.clientY);if(hit)openPreview(hit)}
};
stage.addEventListener('pointerup',up);stage.addEventListener('pointercancel',up);
stage.addEventListener('pointerleave',()=>cur.classList.add('off'));
addEventListener('wheel',e=>{e.preventDefault();if(pv.open)return;vel.x+=e.deltaX*.09;vel.y+=e.deltaY*.09;idle=0},{passive:false});
addEventListener('keydown',e=>{if(e.key==='Escape'&&pv.open&&!pv.busy)closePreview();if(pv.open)return;const k={ArrowLeft:[-26,0],ArrowRight:[26,0],ArrowUp:[0,-26],ArrowDown:[0,26]}[e.key];if(k){vel.x+=k[0];vel.y+=k[1];idle=0}});
addEventListener('resize',resize);
async function boot(){
if(!gl)return;
const sh=(ty,src)=>{const s=gl.createShader(ty);gl.shaderSource(s,src);gl.compileShader(s);return s};
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VS));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FS));gl.linkProgram(pr);gl.useProgram(pr);
gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);gl.bindAttribLocation(pr,0,'a');gl.linkProgram(pr);gl.useProgram(pr);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
for(const n of['T','W','R','P','V','S','Z','O','M','F','Q','CELL','CW','CH','GC'])U[n]=gl.getUniformLocation(pr,n);
gl.clearColor(.035,.035,.043,1);gl.clear(gl.COLOR_BUFFER_BIT);
await Promise.all([document.fonts.load('500 20px Outfit'),document.fonts.load('400 13px Inter')]);
const max=gl.getParameter(gl.MAX_TEXTURE_SIZE);tileScale=Math.min(1.6,dpr*1.1,max/TW,max/TH);
const tile=drawTile(tileScale);
gl.bindTexture(gl.TEXTURE_2D,gl.createTexture());
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,tile);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.REPEAT);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.REPEAT);
gl.activeTexture(gl.TEXTURE1);gl.bindTexture(gl.TEXTURE_2D,gl.createTexture());
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([9,9,11,255]));
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
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.activeTexture(gl.TEXTURE0);
gl.uniform1i(U.T,0);gl.uniform1i(U.W,1);gl.uniform2f(U.S,TW,TH);gl.uniform1f(U.O,0);
resize();
if(!calm){pos.x-=860;pos.y+=560;vel.x=47;vel.y=-31;blur.x=113;blur.y=-74}
render();requestAnimationFrame(frame);
}
boot();
```
## 13. Feel
A wall of living websites with weight and speed. Throw it and it tears into colour, let go and it lands perfectly sharp, click one and that exact video pours out to fill the screen.[/hidecontent]