
Sextante · Relojería de autor
ProComercio · Estudios creativos
La pieza
Landing completa de una startup ficticia de aceleradores de inferencia (Silíceo, Parque Tecnológico de Norvik). En el hero, una oblea de 300 mm con 562 chips en relieve flota en negro, se inclina hacia el cursor con un muelle y hace correr bandas de arcoíris bajo tiras de luz de sala blanca. Al desplazarse, la oblea se encara y un zoom continuo la atraviesa: chip, bloque de núcleos, enrutado (con la pantalla inundada de ámbar de fotolitografía) y transistor, con barra de escala y aumento en vivo. Siguen una inferencia simulada con pulsos por el circuito, cuatro métricas que cuentan, un servidor 1U destapado donde el encapsulado cae en su zócalo y la placa se calienta, un mapa de Norvik trazado a línea, un formulario que calcula TOPS y potencia, y un pie con el render del chip en parallax. Usa recursos propios de Motionframe (tres GLB con Draco, un HDRI de sala blanca, renders AVIF/WebP con profundidad y un loop de vídeo) servidos desde motionframe-two.vercel.app; se pueden sustituir por los del cliente cambiando la constante ASSETS y respetando los nombres, o cargando sus propios GLB con los mismos nombres de nodo. Encaja en hardware, semiconductores, IA, deep tech e industria de precisión.
Encaja bien en webs de
Antes de copiar
Cambia lo que quieras y copia: el prompt sale ya con el nombre, los colores y los textos de tu cliente. Lo que dejes en blanco se queda como en la demo.
Gratis y entero
Qué trae el prompt, por apartados
Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.
# Recrea en un único archivo HTML: Silíceo · chips de inferencia de 3 nm (landing Signature)
Una landing de marca de semiconductores en negro: una oblea de 300 mm iridiscente que se inclina hacia el cursor, un zoom continuo de la oblea al transistor que inunda la pantalla de ámbar, y un encapsulado que cae en un servidor. Sensación de spot tecnológico de estudio, no de plantilla.
Eres un desarrollador front-end creativo senior. Genera un único `index.html` autocontenido que reproduzca exactamente la pieza descrita: mismo layout, textos, colores, movimiento e interacción. HTML, CSS y JS en un solo archivo, sin build ni frameworks. Librerías solo por CDN y en las versiones indicadas. Todos los valores numéricos de este documento son definitivos: úsalos tal cual. Responde solo con el archivo completo.
## Qué es
Es la landing de Silíceo, una empresa ficticia de Norvik que diseña el S1, un acelerador de inferencia de 3 nm. La página es un viaje de escala en una sola pieza continua, con un único reloj de scroll (Lenis alimenta a ScrollTrigger) y un único `<canvas>` fijo detrás del DOM que dibuja, según el tramo de la página, una de cuatro escenas:
1. **La oblea** (hero): objeto héroe real con masa y material físico. Un GLB propio de una oblea de 300 mm con 562 chips en relieve, `MeshPhysicalMaterial` con iridiscencia de capa fina, un HDRI propio de «sala blanca» (siete tiras de luz fría en el techo y una bahía de fotolitografía en ámbar) y un término de difracción inyectado con `onBeforeCompile` que hace correr bandas de arcoíris cuando la oblea se inclina. La oblea sigue al cursor con un muelle (tensión 170, fricción 26). En táctil se arrastra con el dedo o sigue al giroscopio y, si nadie la toca, se mece sola.
2. **El zoom de 10⁸** (sección fija de 520 svh): un solo `ShaderMaterial` a pantalla completa cuya única variable de escala es `uLog` (log₁₀ del ancho de pantalla en milímetros). Cinco capas procedurales se funden con la escala: oblea (300 mm) → chip (10,5 mm, con el plano de planta leído de una textura propia) → bloque de núcleos (1,2 mm) → enrutado (4 µm, baldosas Manhattan de cobre con vías) → transistor (3 nm, aletas y puertas). En el tramo del enrutado la pantalla entera se inunda del ámbar de la fotolitografía y la interfaz pasa a negro. HUD con barra de escala viva, aumento y etapas.
3. **Inferencia en directo**: el mismo shader del enrutado sirve de fondo, con pulsos cian recorriendo las pistas mientras una consola escribe la respuesta token a token y un contador marca los tokens por segundo (todo simulado, sin red).
4. **Del silicio al centro de datos** (sección fija de 320 svh): un servidor 1U destapado (GLB propio) y el encapsulado del S1 (GLB propio) que cae y encaja en el zócalo A; después arrancan los ventiladores, se encienden 24 LED en ola y un mapa de calor (shader aditivo) se extiende por la placa. La cámara orbita desde atrás y se acerca al zócalo. Los materiales del GLB se sustituyen por nombre al cargar (aluminio cepillado con mapa de rugosidad procedural, placa con máscara verde oscura, serigrafía y pads dorados pintados en un canvas, cobre, plástico), el foco proyecta el único mapa de sombras de la pieza y un disco de sombra de contacto se concentra bajo el chip al encajar.
Entre medias hay secciones DOM: cuatro cifras que cuentan con un gráfico SVG que se dibuja sobre un loop de vídeo propio, la sección clara de Norvik con un mapa a línea que se traza y ofertas de empleo, el formulario del kit de desarrollo que calcula TOPS, potencia y ocupación, y el pie con el render del encapsulado en parallax por mapa de profundidad y una marquesina gigante.
Tipografía: **Chivo 300** como display (titulares de 14 vw, partidos en esquinas opuestas) + **Chivo 400** para texto + **Chivo Mono** para datos y HUD. Paleta: negro `#050608`, silicio `#8c93a3`, hueso `#eef0f4`, ámbar `#ffb000` y cian de datos `#3be8ff`. Intro con concepto de 1,7 s (una retícula de exposición ámbar se proyecta y una barra la recorre mientras cuenta hasta el 100 %), que se salta con cualquier tecla o toque y nunca dura más de 2,4 s.
## Librerías y esqueleto
Exactamente estas etiquetas en `<head>`, en este orden (fuentes con `display=swap`; Chivo y Chivo Mono tienen ñ, tildes, ¿ y ¡):
```html
<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=Chivo:wght@300;400;600&family=Chivo+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.css">
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrambleTextPlugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.min.js"></script>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
} }
</script>
```
- `three` 0.186.1 por import map (`"three"` y `"three/addons/"`), con `GLTFLoader`, `DRACOLoader`, `HDRLoader` y `PMREMGenerator`. Decodificador Draco: `https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/libs/draco/gltf/`.
- GSAP 3.15.0 con `ScrollTrigger` y `ScrambleTextPlugin` (scripts clásicos, globales `gsap`, `ScrollTrigger`, `ScrambleTextPlugin`).
- Lenis 1.3.26 (script clásico, global `Lenis`) y su `lenis.css`.
- `<html lang="es">`, `<meta charset="utf-8">`, viewport con `viewport-fit=cover`. Todo el JS de la pieza va en un único `<script type="module">` al final del `<body>`.
- Título y descripción literales: `<title>Silíceo · Chips de inferencia de 3 nm diseñados en Norvik</title>` y `<meta name="description" content="Silíceo diseña el S1, un acelerador de inferencia de 3 nm. Una oblea de 300 mm, un zoom de 10⁸ y un centro de datos, en una sola página.">`.
- Alturas con `svh`; nada de `100vh` a secas. La página funciona dentro de un iframe: sin `window.top`, sin `alert`, sin `localStorage`.
## Recursos
Todos los recursos son propios de Motionframe y se sirven desde la base `https://motionframe.app/assets/signature-siliceo` (URL absoluta, sin barra final). **No sustituyas estos recursos por otros ni los generes: cárgalos tal cual.** En el código hay una sola constante y ninguna otra ruta a recursos en todo el archivo:
```js
const ASSETS = "https://motionframe.app/assets/signature-siliceo";
const A = (n) => `${ASSETS}/${n}`;
```
| URL | Formato | Dimensiones / contenido | Peso | Para qué se usa | Si falla |
|---|---|---|---|---|---|
| `https://motionframe.app/assets/signature-siliceo/oblea-v1.glb` | GLB (Draco) | Ø 3,0 u · 16.996 triángulos · 2 materiales (Silicio, Chip) | 89 KB | Objeto héroe del hero y del empalme con el zoom | Se queda el póster AVIF/WebP del hero (clase sin-glb) y la página sigue |
| `https://motionframe.app/assets/signature-siliceo/sala-blanca-v1.hdr` | Radiance HDR lineal | 1024 × 512 | 945 KB | Entorno PMREM de la oblea y del servidor (tiras de luz y bahía ámbar) | Una luz direccional y una ambiental (código incluido) |
| `https://motionframe.app/assets/signature-siliceo/chip-normal-v1.webp` | WebP sin pérdida | 1024 × 1024, mapa de normales tangente (OpenGL, +Y arriba) | 8 KB | normalMap del material Chip (UV 0–1 por chip) | Sin relieve; nada más cambia |
| `https://motionframe.app/assets/signature-siliceo/chip-mapa-v1.webp` | WebP sin pérdida | 1024 × 1024 · R altura, G tipo de bloque (÷7), B pistas | 2 KB | Plano de planta del chip en la capa L1 del zoom (uMapa) | uMapaOk = 0: la capa L1 pinta gris azulado plano |
| `https://motionframe.app/assets/signature-siliceo/chip-v1.glb` | GLB (Draco) | 0,52 u de lado · nodos Sustrato, Tapa, Die, Condensadores, Bola | 7 KB | Encapsulado que cae en el servidor; Bola se instancia 30 × 30 | La escena del servidor se ve sin chip |
| `https://motionframe.app/assets/signature-siliceo/servidor-v1.glb` | GLB (Draco) | 4,4 × 7,0 × 0,44 u · nodos Chasis, Frontal, Placa, Zocalo_A/B, Disipador, DIMM, Componentes, PCH, Pistas, Ventilador_1…6, Fuente | 48 KB | Escena «del silicio al centro de datos» | El póster chip-macro sustituye a la escena |
| `https://motionframe.app/assets/signature-siliceo/hero-poster-v1.avif` | AVIF q50 | 1920 × 1080 | 47 KB | Póster del hero (sin WebGL, sin GLB) y ≥ 900 px | — |
| `https://motionframe.app/assets/signature-siliceo/hero-poster-v1.webp` | WebP q84 | 1920 × 1080 | 117 KB | Respaldo del anterior en navegadores sin AVIF | — |
| `https://motionframe.app/assets/signature-siliceo/hero-poster-1280-v1.webp` | WebP q80 | 1280 × 720 | 59 KB | Póster del hero en < 900 px | — |
| `https://motionframe.app/assets/signature-siliceo/hero-profundidad-v1.webp` | WebP sin pérdida (gris) | 1920 × 1080, disparidad (blanco = cerca) | 43 KB | Reservado: parallax del póster si se quiere en el fallback | — |
| `https://motionframe.app/assets/signature-siliceo/chip-macro-v1.avif` | AVIF q50 | 1920 × 1080 | 13 KB | Fondo del pie con parallax por profundidad; póster del servidor sin WebGL | Se usa el WebP |
| `https://motionframe.app/assets/signature-siliceo/chip-macro-v1.webp` | WebP q84 | 1920 × 1080 | 28 KB | Respaldo del anterior | — |
| `https://motionframe.app/assets/signature-siliceo/chip-macro-1280-v1.webp` | WebP q80 | 1280 × 720 | 15 KB | Pie y póster del servidor en < 900 px | — |
| `https://motionframe.app/assets/signature-siliceo/chip-macro-profundidad-v1.webp` | WebP sin pérdida (gris) | 1920 × 1080, disparidad | 99 KB | uDep del shader del pie (6 iteraciones de punto fijo) | uOk = 0: el pie queda negro con la marquesina |
| `https://motionframe.app/assets/signature-siliceo/oblea-loop-v1.webm` | WebM VP9 crf 35 | 1280 × 720 · 4 s · 30 fps · bucle perfecto | 475 KB | Vídeo de fondo de «Métricas» (se carga al acercarse) | El póster WebP |
| `https://motionframe.app/assets/signature-siliceo/oblea-loop-v1.mp4` | MP4 H.264 crf 22 | 1280 × 720 · 4 s | 776 KB | Respaldo del WebM | — |
| `https://motionframe.app/assets/signature-siliceo/oblea-loop-poster-v1.webp` | WebP q78 | 1280 × 720 | 21 KB | Póster del loop y sustituto en movimiento reducido | — |
Total: unos 2791 KB; el hero está listo con la oblea, el HDRI, el mapa de normales y el póster (89 KB + 945 KB + 8 KB + 47 KB). Los `<picture>` de respaldo llevan `srcset`/`src` vacíos y un `data-src` con el nombre del archivo; al arrancar, el JS les pone `A(nombre)`. Los GLB llevan los nodos y materiales con los nombres indicados: el código los busca por nombre (`/chip/i` en el nombre del material de la oblea; `/^Ventilador_\d/` en el servidor; `Bola` y `Die` en el chip).
Orden de carga (prioridades): primero `oblea-v1.glb`, `sala-blanca-v1.hdr` y `chip-normal-v1.webp` (lo que necesita el hero, mientras la intro corre); el zoom, el servidor, el encapsulado y el pie se crean en `requestIdleCallback` con `timeout: 1200` (la escena principal satura el hilo y sin el timeout el callback puede no llegar); el vídeo de «Métricas» solo recibe sus `<source>` cuando la sección está a 150 % del viewport. Si un GLB o una textura fallan, se ve el respaldo (póster o degradado) y la página sigue funcionando.
## Tokens de diseño
Bloque `:root` literal (los tres colores de marca son variables del personalizador; los shaders los leen al arrancar con `getComputedStyle`, así el cliente los cambia sin tocar GLSL):
```css
:root{
--negro:#050608; --negro-2:#0b0d12; --silicio:#8c93a3; --hueso:#eef0f4; --tinta:#0a0c10;
--tinta-suave:#6f7683; --ambar:#ffb000; --cian:#3be8ff; --linea:rgba(238,240,244,.12);
--display:"Chivo",system-ui,sans-serif; --mono:"Chivo Mono",ui-monospace,Menlo,monospace;
--gutter:clamp(16px,3.2vw,52px); --ease-out:cubic-bezier(.16,1,.3,1);
}
```
Con `body.ambar-on` (se activa en el tramo ámbar del zoom) se redefinen `--hueso:#0a0c10`, `--linea:rgba(10,12,16,.16)` y `--tinta-suave:#4a3a10`: la interfaz pasa a negro sobre el ámbar.
## Estructura y contenido
Hoja de estilos completa (literal; contiene el layout de todas las secciones, los breakpoints y los estados `estatico`, `no-webgl`, `sin-glb` y `ambar-on`):
```css
:root{
--negro:#050608; --negro-2:#0b0d12; --silicio:#8c93a3; --hueso:#eef0f4; --tinta:#0a0c10;
--tinta-suave:#6f7683; --ambar:#ffb000; --cian:#3be8ff; --linea:rgba(238,240,244,.12);
--display:"Chivo",system-ui,sans-serif; --mono:"Chivo Mono",ui-monospace,Menlo,monospace;
--gutter:clamp(16px,3.2vw,52px); --ease-out:cubic-bezier(.16,1,.3,1);
}
*{box-sizing:border-box}
html{scroll-behavior:auto;-webkit-text-size-adjust:100%}
html.lenis,html.lenis body{height:auto}
body{margin:0;background:var(--negro);color:var(--hueso);font:400 16px/1.5 var(--display);overflow-x:hidden;
-webkit-font-smoothing:antialiased}
body.ambar-on{--hueso:#0a0c10;--linea:rgba(10,12,16,.16);--tinta-suave:#4a3a10}
a{color:inherit}
button,input,select{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--cian);outline-offset:3px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
#gl{position:fixed;inset:0;width:100%;height:100%;z-index:0;display:block}
main{position:relative;z-index:1}
.mono{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;line-height:1.5}
.mono b{font-weight:500;color:var(--hueso)}
/* ---------- intro ---------- */
#intro{position:fixed;inset:0;z-index:60;background:var(--negro);display:grid;place-items:center;overflow:hidden;
clip-path:inset(0 0 0 0)}
#intro .ret{position:absolute;inset:0;background-image:linear-gradient(rgba(255,176,0,.55) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,176,0,.55) 1px,transparent 1px);background-size:3.85vw 3.85vw;opacity:0;
mask-image:radial-gradient(70% 70% at 50% 50%,#000 30%,transparent 100%)}
#intro .barra{position:absolute;left:0;right:0;top:-2px;height:2px;background:var(--ambar);
box-shadow:0 0 24px 4px rgba(255,176,0,.6);transform:translateY(-4px)}
#intro .marca{font-weight:300;font-size:clamp(40px,7vw,120px);letter-spacing:-.04em;line-height:1;color:var(--ambar);
display:flex;gap:.02em}
#intro .marca span{display:inline-block;opacity:0;filter:blur(10px);transform:translateY(.3em)}
#intro .txt{position:absolute;left:var(--gutter);bottom:calc(var(--gutter) + env(safe-area-inset-bottom));color:var(--ambar)}
#intro .txt2{position:absolute;right:var(--gutter);bottom:calc(var(--gutter) + env(safe-area-inset-bottom));color:var(--ambar);text-align:right}
/* ---------- nav ---------- */
.nav{position:fixed;inset:0 0 auto 0;z-index:40;display:flex;align-items:center;justify-content:space-between;
padding:18px var(--gutter);pointer-events:none;transition:color .4s}
.nav>*{pointer-events:auto}
/* velo de la nav: solo aparece sobre las secciones DOM (nunca sobre las escenas WebGL) */
.nav::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:0;transition:opacity .4s;
background:linear-gradient(180deg,rgba(5,6,8,.78),rgba(5,6,8,0));backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
mask-image:linear-gradient(#000 45%,transparent);-webkit-mask-image:linear-gradient(#000 45%,transparent)}
body.nav-solida .nav::before{opacity:1}
body.claro-on .nav{color:var(--tinta)}
body.claro-on .nav::before{background:linear-gradient(180deg,rgba(238,240,244,.86),rgba(238,240,244,0))}
.logo{display:flex;align-items:center;gap:10px;text-decoration:none;font-weight:600;letter-spacing:.02em;font-size:15px}
.logo svg{width:22px;height:22px}
.nav ul{display:flex;gap:26px;list-style:none;margin:0;padding:0}
.nav ul a{text-decoration:none;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;opacity:.8;
display:inline-block;min-width:var(--w,auto);transition:opacity .3s}
.nav ul a:hover{opacity:1}
/* botones: relleno que barre de izquierda a derecha (hover y foco) */
.btn{position:relative;isolation:isolate;overflow:hidden;display:inline-flex;align-items:center;gap:10px;border:1px solid currentColor;border-radius:999px;padding:10px 18px;
text-decoration:none;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;background:transparent;
cursor:pointer;transition:color .35s}
.btn::before{content:"";position:absolute;inset:-1px;z-index:-1;background:var(--hueso);transform:scaleX(0);transform-origin:left;
transition:transform .5s var(--ease-out)}
.btn:hover::before,.btn:focus-visible::before{transform:scaleX(1)}
.btn:hover,.btn:focus-visible{color:var(--negro)}
.btn.lleno{background:var(--ambar);border-color:var(--ambar);color:var(--negro)}
.btn.lleno::before{background:var(--hueso)}
body.ambar-on .btn:hover,body.ambar-on .btn:focus-visible{color:var(--ambar)}
@media (max-width:820px){.nav ul{display:none}}
/* ---------- hero ---------- */
#hero{position:relative;height:100svh;min-height:600px;overflow:hidden}
#hero .poster{position:absolute;inset:0;z-index:0;opacity:0;transition:opacity .8s}
#hero .poster img{width:100%;height:100%;object-fit:cover;object-position:60% 50%}
body.no-webgl #hero .poster,body.sin-glb #hero .poster{opacity:1}
.hud{position:absolute;z-index:2;pointer-events:none;color:var(--hueso)}
.hud.tl{left:var(--gutter);top:calc(var(--gutter) + 52px)}
.hud.tr{right:var(--gutter);top:calc(var(--gutter) + 52px);text-align:right}
.hud.bl{left:var(--gutter);bottom:var(--gutter)}
.hud.br{right:var(--gutter);bottom:var(--gutter);text-align:right}
.hud .mono{opacity:.72}
.hud .mono b{opacity:1}
.titular{position:absolute;z-index:2;margin:0;font-weight:300;letter-spacing:-.045em;line-height:.86;
font-size:clamp(64px,14vw,240px);pointer-events:none}
.titular .l{display:inline-block;white-space:pre}
.titular .l span{display:inline-block;will-change:transform,filter}
#t1{left:var(--gutter);top:22vh}
#t2{right:var(--gutter);bottom:18vh;text-align:right}
#t2 em{font-style:normal;color:var(--ambar)}
.hero-p{position:absolute;z-index:2;left:var(--gutter);bottom:calc(var(--gutter) + 92px);max-width:34ch;margin:0;
font-size:clamp(14px,1.15vw,18px);color:var(--silicio);opacity:0}
.hero-p b{color:var(--hueso);font-weight:400}
.cursor{position:fixed;z-index:45;left:0;top:0;width:34px;height:34px;border:1px solid var(--cian);border-radius:50%;
pointer-events:none;transform:translate(-50%,-50%);opacity:0;transition:opacity .3s,width .3s,height .3s;mix-blend-mode:screen}
.cursor::after{content:attr(data-t);position:absolute;left:24px;top:-4px;font:500 10px/1 var(--mono);letter-spacing:.1em;
text-transform:uppercase;color:var(--cian);white-space:nowrap}
body.puntero-ok .cursor.visible{opacity:1}
@media (max-width:820px){
#hero{min-height:640px}
#t1{top:13vh}#t2{bottom:24vh}
.hud.tr,.hud.br{display:none}
.hero-p{bottom:calc(var(--gutter) + 58px);max-width:26ch;font-size:13px;line-height:1.45}
.hud.bl .mono{font-size:10px}
}
/* ---------- zoom ---------- */
#zoom{position:relative;height:520svh}
#zoom .fijo{position:sticky;top:0;height:100svh;overflow:hidden}
/* velo local detrás del texto: el circuito de cobre satura y la frase se perdía; en el tramo ámbar el velo es ámbar claro (texto negro) */
.zoom-velo{position:absolute;inset:0;pointer-events:none;
background:linear-gradient(0deg,rgba(5,6,8,.74) 0,rgba(5,6,8,.38) 24%,transparent 48%),linear-gradient(180deg,rgba(5,6,8,.5) 0,transparent 24%)}
body.ambar-on .zoom-velo{background:linear-gradient(0deg,rgba(255,206,92,.8) 0,rgba(255,206,92,.42) 24%,transparent 48%),linear-gradient(180deg,rgba(255,206,92,.6) 0,transparent 24%)}
.escala-num{position:absolute;left:var(--gutter);bottom:calc(var(--gutter) + 64px);margin:0;font-weight:300;
letter-spacing:-.05em;line-height:.9;font-size:clamp(72px,13vw,220px);pointer-events:none;color:var(--hueso)}
.escala-num small{font-size:.42em;letter-spacing:-.02em}
.escala-txt{position:absolute;left:var(--gutter);bottom:var(--gutter);margin:0;max-width:44ch;color:var(--hueso);opacity:.92;overflow-wrap:anywhere}
.zoom-hud{position:absolute;right:var(--gutter);top:calc(var(--gutter) + 52px);text-align:right;color:var(--hueso)}
.zoom-hud .aum{font-family:var(--mono);font-size:clamp(18px,2.2vw,30px);letter-spacing:0;text-transform:none;margin-top:6px}
.barra-escala{position:absolute;right:var(--gutter);bottom:var(--gutter);width:22vw;min-width:120px;color:var(--hueso)}
.barra-escala .l{height:1px;background:currentColor;position:relative;margin-bottom:8px}
.barra-escala .l::before,.barra-escala .l::after{content:"";position:absolute;top:-5px;width:1px;height:11px;background:currentColor}
.barra-escala .l::after{right:0}
.barra-escala .v{font-family:var(--mono);font-size:14px;letter-spacing:.04em;text-transform:none}
.zoom-etapas{position:absolute;left:var(--gutter);top:calc(var(--gutter) + 52px);display:grid;gap:6px;color:var(--hueso)}
.zoom-etapas span{opacity:.35;transition:opacity .3s}
.zoom-etapas span.on{opacity:1}
.zoom-etapas span::before{content:"";display:inline-block;width:18px;height:1px;background:currentColor;margin-right:10px;vertical-align:middle}
.estados{display:none;padding:12vh var(--gutter);gap:4vh}
.estado{display:grid;grid-template-columns:1fr 1fr;gap:4vw;align-items:center;min-height:60svh;border-top:1px solid var(--linea);padding:6vh 0}
.estado .cuadro{aspect-ratio:16/10;border:1px solid var(--linea)}
.estado h3{font-weight:300;font-size:clamp(44px,8vw,120px);margin:0 0 12px;letter-spacing:-.04em;line-height:.9}
body.estatico #zoom{height:auto}
body.estatico #zoom .fijo{display:none}
body.estatico .estados{display:grid}
@media (max-width:820px){
.zoom-etapas{display:none}.escala-num{bottom:calc(var(--gutter) + 110px)}.escala-txt{max-width:80vw;font-size:14px}
.barra-escala{width:32vw;bottom:calc(var(--gutter) + 70px)}.estado{grid-template-columns:1fr}
}
/* ---------- inferencia ---------- */
#inferencia{position:relative;min-height:100svh;padding:calc(var(--gutter) + 96px) var(--gutter) 12vh;display:grid;grid-template-columns:1fr 1fr;gap:5vw;align-items:start}
#inferencia::before{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(180deg,rgba(5,6,8,0) 0,rgba(5,6,8,.25) 30%,rgba(5,6,8,.45) 100%)}
h2.sec{font-weight:300;font-size:clamp(44px,7vw,120px);letter-spacing:-.045em;line-height:.9;margin:0 0 24px}
h2.sec em{font-style:normal;color:var(--ambar)}
.lead{color:var(--silicio);max-width:46ch;font-size:clamp(15px,1.2vw,19px);margin:0}
.consola{border:1px solid var(--linea);background:rgba(5,6,8,.72);backdrop-filter:blur(10px);padding:22px;display:grid;gap:16px}
.consola form{display:flex;gap:10px}
.consola input{flex:1;background:transparent;border:0;border-bottom:1px solid var(--linea);padding:10px 0;font-size:16px;min-width:0}
.consola input::placeholder{color:var(--tinta-suave)}
.salida{min-height:9lh;max-height:12lh;overflow:hidden;font-size:clamp(15px,1.2vw,18px);line-height:1.55;color:var(--hueso);white-space:pre-wrap}
.salida .c{display:inline-block;width:.55em;height:1em;background:var(--cian);vertical-align:-.15em;animation:parp 1s steps(2) infinite}
@keyframes parp{50%{opacity:0}}
.tok{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;border-top:1px solid var(--linea);padding-top:14px}
.tok b{display:block;font:500 clamp(20px,2vw,28px)/1 var(--mono);letter-spacing:0;text-transform:none;color:var(--cian);margin-bottom:4px}
.tok .mono{opacity:.7}
@media (max-width:820px){#inferencia{grid-template-columns:1fr;align-items:start;gap:8vh}}
/* ---------- métricas ---------- */
#metricas{position:relative;min-height:100svh;padding:calc(var(--gutter) + 96px) var(--gutter) 16vh;overflow:hidden;background:var(--negro)}
#metricas video,#metricas .loop-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.75;z-index:0;filter:saturate(1.15) contrast(1.08)}
#metricas::after{content:"";position:absolute;inset:0;z-index:0;background:radial-gradient(80% 60% at 70% 40%,rgba(5,6,8,0),rgba(5,6,8,.88) 78%),linear-gradient(180deg,rgba(5,6,8,.55),transparent 30%,transparent 70%,rgba(5,6,8,.6))}
#metricas>*{position:relative;z-index:1}
.cifras{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;margin:10vh 0 12vh;border-top:1px solid var(--linea)}
.cifra{padding-top:22px;border-right:1px solid var(--linea);padding-right:18px;display:grid;gap:10px;align-content:start}
.cifra:last-child{border-right:0}
.cifra b{display:block;font-weight:300;font-size:clamp(44px,5.4vw,96px);letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums;
text-shadow:0 2px 30px rgba(0,0,0,.6)}
.cifra b small{font-size:.38em;letter-spacing:0;color:var(--silicio);margin-left:.15em}
.cifra .mono{color:var(--silicio)}
/* micrográfico bajo cada cifra: una línea que se traza al llegar */
.cifra .mini{width:100%;max-width:150px;height:34px;display:block;overflow:visible}
.cifra .mini polyline{fill:none;stroke:var(--ambar);stroke-width:1.5;stroke-linejoin:round;stroke-linecap:round}
.cifra .mini circle{fill:var(--ambar)}
.cifra .mini line{stroke:var(--linea)}
.grafico{display:grid;grid-template-columns:1fr 2fr;gap:4vw;align-items:end}
.grafico svg{width:100%;height:auto;overflow:visible}
.grafico text{font-family:var(--mono);font-size:11px;letter-spacing:.08em;fill:var(--silicio)}
.grafico .val{fill:var(--hueso);font-size:14px}
@media (max-width:820px){.cifras{grid-template-columns:1fr 1fr}.cifra:nth-child(2){border-right:0}.grafico{grid-template-columns:1fr}}
/* ---------- servidor ---------- */
#servidor{position:relative;height:320svh}
#servidor .fijo{position:sticky;top:0;height:100svh;overflow:hidden}
#servidor .poster{position:absolute;inset:0;display:none}
body.no-webgl #servidor .poster{display:block}
#servidor .poster img{width:100%;height:100%;object-fit:cover;opacity:.5}
/* viñeta cinematográfica: profundidad sin pagar un pase de DoF */
.srv-velo{position:absolute;inset:0;pointer-events:none;background:radial-gradient(85% 75% at 50% 48%,transparent 50%,rgba(5,6,8,.72) 100%)}
.srv-t1{left:var(--gutter);top:calc(var(--gutter) + 64px);font-size:clamp(48px,8vw,140px);text-shadow:0 2px 24px rgba(0,0,0,.5)}
.srv-t2{right:var(--gutter);bottom:calc(var(--gutter) + 70px);text-align:right;font-size:clamp(48px,8vw,140px);text-shadow:0 2px 24px rgba(0,0,0,.5)}
.srv-hud{position:absolute;left:var(--gutter);bottom:var(--gutter);display:grid;grid-template-columns:repeat(3,auto);gap:28px;color:var(--hueso);text-shadow:0 2px 14px rgba(0,0,0,.7)}
.srv-hud b{display:block;font:500 clamp(18px,2vw,28px)/1 var(--mono);text-transform:none;letter-spacing:0;margin-bottom:6px;font-variant-numeric:tabular-nums}
.srv-p{position:absolute;right:var(--gutter);top:calc(var(--gutter) + 72px);max-width:34ch;text-align:right;color:var(--silicio);margin:0;font-size:clamp(14px,1.1vw,17px)}
@media (max-width:820px){.srv-p{display:none}.srv-hud{grid-template-columns:1fr 1fr;gap:14px}.srv-t2{bottom:calc(var(--gutter) + 120px)}}
/* ---------- norvik ---------- */
#norvik{background:var(--hueso);color:var(--tinta);padding:14vh var(--gutter);display:grid;grid-template-columns:1.1fr .9fr;gap:5vw;position:relative;z-index:2}
#norvik h2.sec em{color:#c47f00}
#norvik .mono{color:#5b6270}
.mapa{border:1px solid rgba(10,12,16,.14);aspect-ratio:4/3;position:relative;overflow:hidden;background:
radial-gradient(90% 70% at 30% 80%,#dfe4ec,transparent 70%),linear-gradient(#f4f5f8,#e9ecf1)}
.mapa svg{position:absolute;inset:0;width:100%;height:100%}
.mapa path,.mapa circle,.mapa line{stroke:#0a0c10;fill:none;stroke-width:1.2;vector-effect:non-scaling-stroke}
.mapa .mar{stroke:#7f8fa8;stroke-width:1}
.mapa .pta{fill:var(--ambar);stroke:none}
.mapa text{font-family:var(--mono);font-size:10px;letter-spacing:.1em;fill:#0a0c10}
.empleo{list-style:none;margin:32px 0 0;padding:0;border-top:1px solid rgba(10,12,16,.18)}
.empleo li a{display:grid;grid-template-columns:1fr auto auto;gap:18px;align-items:center;padding:16px 0;border-bottom:1px solid rgba(10,12,16,.18);text-decoration:none;transition:padding .3s var(--ease-out)}
.empleo li a:hover{padding-left:10px}
.empleo li a span:first-child{font-size:clamp(17px,1.5vw,22px)}
.empleo .fl{width:26px;height:26px;border:1px solid currentColor;border-radius:50%;display:grid;place-items:center;font-size:12px;
transition:background .3s,color .3s,transform .4s var(--ease-out)}
.empleo li a:hover .fl{background:var(--tinta);color:var(--hueso);transform:translateX(4px)}
.dir{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:28px}
.dir p{margin:0}
@media (max-width:820px){#norvik{grid-template-columns:1fr}.empleo li a{grid-template-columns:1fr auto}.empleo li a .mono{display:none}}
/* ---------- kit + footer ---------- */
#kit{position:relative;background:var(--negro);padding:14vh var(--gutter) 12vh;display:grid;grid-template-columns:1fr 1fr;gap:5vw;z-index:2}
.kit-form{display:grid;gap:18px;border:1px solid var(--linea);padding:26px;background:var(--negro-2)}
.campo{display:grid;gap:6px}
.campo label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--silicio)}
.campo input,.campo select{background:transparent;border:0;border-bottom:1px solid var(--linea);padding:10px 0;font-size:16px;color:var(--hueso)}
.campo select option{color:#000}
.campo input[type=range]{padding:0;accent-color:var(--ambar)}
.calc{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;border-top:1px solid var(--linea);padding-top:16px}
.calc b{display:block;font:500 clamp(18px,1.8vw,26px)/1 var(--mono);color:var(--ambar);margin-bottom:4px;font-variant-numeric:tabular-nums}
.gracias{display:none;padding:10px 0;color:var(--cian)}
footer{position:relative;height:100svh;min-height:560px;overflow:hidden;color:var(--hueso)}
footer .poster{position:absolute;inset:0;display:none}
body.no-webgl footer .poster{display:block}
footer .poster img{width:100%;height:100%;object-fit:cover;opacity:.55}
.marquesina{position:absolute;left:0;right:0;bottom:calc(var(--gutter) + 78px);overflow:hidden;white-space:nowrap;pointer-events:none;
font-weight:300;font-size:clamp(56px,11.5vw,200px);letter-spacing:-.04em;line-height:.9}
.marquesina div{display:inline-block;padding-right:.4em;animation:marq 26s linear infinite}
.marquesina em{font-style:normal;color:var(--ambar)}
@keyframes marq{to{transform:translateX(-50%)}}
.pie{position:absolute;left:var(--gutter);right:var(--gutter);bottom:var(--gutter);display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px;align-items:end}
.pie ul{list-style:none;margin:0;padding:0;display:grid;gap:4px}
.pie a{text-decoration:none}
.pie a:hover{text-decoration:underline;text-underline-offset:4px}
.pie .mono{opacity:.8}
@media (max-width:820px){#kit{grid-template-columns:1fr}.calc{grid-template-columns:1fr 1fr}.pie{grid-template-columns:1fr 1fr}.pie>:last-child{display:none}}
@media (prefers-reduced-motion:reduce){
.marquesina div{animation:none}.salida .c{animation:none}
#intro{display:none}
.btn::before,.empleo .fl,.nav ul a{transition:none}
}
```
Cuerpo completo con todos los textos (literal). Las letras de la marca en la intro van en `<span>` sueltos para animarlas una a una; en los titulares, `partir()` (ver Código clave) convierte cada carácter en un `<span>`:
```html
<canvas id="gl" aria-hidden="true"></canvas>
<div class="cursor" aria-hidden="true" data-t="inclinar"></div>
<div id="intro" aria-hidden="true">
<div class="ret"></div>
<div class="barra"></div>
<div class="marca"><span>S</span><span>I</span><span>L</span><span>Í</span><span>C</span><span>E</span><span>O</span></div>
<div class="txt mono">Exposición · retícula 26 × 33 mm<br><b id="intro-pct">0 %</b> · lote 26-0912</div>
<div class="txt2 mono">Pulsa cualquier tecla<br>para saltar</div>
</div>
<header class="nav">
<a class="logo" href="#hero" aria-label="Silíceo, inicio">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M9.5 21.7a2.6 2.6 0 0 1 5 0" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M7 9h10M7 12h10M7 15h10M9 7v10M12 7v10M15 7v10" stroke="currentColor" stroke-width=".8" opacity=".7"/></svg>
Silíceo
</a>
<nav aria-label="Secciones"><ul>
<li><a href="#zoom">Escala</a></li><li><a href="#inferencia">Inferencia</a></li><li><a href="#metricas">Métricas</a></li>
<li><a href="#servidor">Centro de datos</a></li><li><a href="#norvik">Norvik</a></li>
</ul></nav>
<a class="btn" href="#kit">Acceso al kit</a>
</header>
<main>
<!-- 1 · HERO -->
<section id="hero" aria-label="Silíceo S1">
<div class="poster" aria-hidden="true">
<picture>
<source type="image/avif" srcset="" data-src="hero-poster-v1.avif" media="(min-width:900px)">
<source type="image/webp" srcset="" data-src="hero-poster-v1.webp" media="(min-width:900px)">
<img alt="" src="" data-src="hero-poster-1280-v1.webp" decoding="async">
</picture>
</div>
<div class="hud tl"><div class="mono">Oblea 300 mm · lote 26-0912<br><b id="hud-ok">562 / 562 chips ok</b></div></div>
<div class="hud tr"><div class="mono">S1 · 3 nm · <b>1.200 TOPS</b><br>Diseñado en Norvik · encapsulado en Europa</div></div>
<h1 class="titular" id="t1"><span class="l">Pensado</span></h1>
<p class="titular" id="t2" aria-hidden="true"><span class="l">en silicio.</span></p> <!-- la última palabra va dentro de <em> (color de acento): en la demo, «en <em>silicio.</em>» -->
<p class="hero-p">Silíceo diseña el <b>S1</b>, un acelerador de inferencia de 3 nm nacido en el Parque Tecnológico de Norvik. Sin fábrica propia: <b>562 chips</b> por oblea, fabricados en Europa y probados en Norvik.</p>
<div class="hud bl"><div class="mono">Hora en Norvik <b id="hud-hora">--:--:--</b><br>00,00° N · 00,00° E · sala blanca ISO 5</div></div>
<div class="hud br"><div class="mono">Inclinación <b id="hud-tilt">+0,0° / +0,0°</b><br><span id="hud-hint">Mueve el cursor · desplázate para entrar en el chip ↓</span></div></div>
</section>
<!-- 2 · ZOOM 10⁸ -->
<section id="zoom" aria-label="Zoom de la oblea al transistor">
<div class="fijo">
<div class="zoom-velo" aria-hidden="true"></div>
<div class="zoom-etapas mono" aria-hidden="true">
<span data-e="0">Oblea · 300 mm</span><span data-e="1">Chip · 10,5 mm</span><span data-e="2">Bloque · 1,2 mm</span>
<span data-e="3">Enrutado · 4 µm</span><span data-e="4">Transistor · 3 nm</span>
</div>
<div class="zoom-hud mono">Aumento<div class="aum" id="zoom-aum">×1</div></div>
<p class="escala-num"><span id="zoom-num">300</span><small id="zoom-uni"> mm</small></p>
<p class="escala-txt" id="zoom-txt">Una oblea de 300 mm con 562 chips S1. Cada uno es un acelerador completo.</p>
<div class="barra-escala" aria-live="off"><div class="l"></div><div class="v" id="barra-v">66 mm</div></div>
</div>
<div class="estados" id="estados"></div>
</section>
<!-- 3 · INFERENCIA -->
<section id="inferencia" aria-labelledby="inf-h">
<div>
<h2 class="sec" id="inf-h">Inferencia<br>en <em>directo.</em></h2>
<p class="lead">Escribe una pregunta. El S1 la responde a más de mil tokens por segundo mientras los pulsos recorren el enrutado. Todo se simula en tu navegador: no hay red ni modelo detrás, solo el ritmo real del chip.</p>
</div>
<div class="consola">
<form id="inf-form">
<label class="sr" for="inf-q">Pregunta</label>
<input id="inf-q" type="text" value="¿Cuánto consume un S1 en reposo?" autocomplete="off" maxlength="120">
<button class="btn lleno" type="submit">Inferir</button>
</form>
<div class="salida" id="inf-out" aria-live="polite"></div>
<div class="tok">
<div><b id="tok-s">0</b><span class="mono">tokens / s</span></div>
<div><b id="tok-n">0 / 0</b><span class="mono">tokens</span></div>
<div><b id="tok-lat">— ms</b><span class="mono">primer token</span></div>
</div>
</div>
</section>
<!-- 4 · MÉTRICAS -->
<section id="metricas" aria-labelledby="met-h">
<video muted loop playsinline preload="none" aria-hidden="true" data-webm="oblea-loop-v1.webm" data-mp4="oblea-loop-v1.mp4" data-poster="oblea-loop-poster-v1.webp"></video>
<h2 class="sec" id="met-h">Cifras que caben<br>en <em>26 × 33 mm.</em></h2>
<div class="cifras">
<div class="cifra"><b><span data-n="1200" data-f="int">0</span><small>TOPS</small></b>
<svg class="mini" viewBox="0 0 150 34" aria-hidden="true"><line x1="0" y1="33" x2="150" y2="33"/><polyline points="0,30 25,27 50,24 75,18 100,12 125,6 150,2"/><circle cx="150" cy="2" r="2.5"/></svg>
<span class="mono">INT8, lote 1, 900 MHz</span></div>
<div class="cifra"><b><span data-n="18.4" data-f="dec1">0</span><small>TOPS/W</small></b>
<svg class="mini" viewBox="0 0 150 34" aria-hidden="true"><line x1="0" y1="33" x2="150" y2="33"/><polyline points="0,24 25,14 50,8 75,5 100,4 125,4 150,5"/><circle cx="150" cy="5" r="2.5"/></svg>
<span class="mono">Eficiencia a 65 W TDP</span></div>
<div class="cifra"><b><span data-n="2.1" data-f="dec1">0</span><small>ms</small></b>
<svg class="mini" viewBox="0 0 150 34" aria-hidden="true"><line x1="0" y1="33" x2="150" y2="33"/><polyline points="0,6 25,9 50,8 75,14 100,18 125,24 150,27"/><circle cx="150" cy="27" r="2.5"/></svg>
<span class="mono">Latencia p99 · modelo de 8.000 M</span></div>
<div class="cifra"><b><span data-n="96" data-f="int">0</span><small>GB</small></b>
<svg class="mini" viewBox="0 0 150 34" aria-hidden="true"><line x1="0" y1="33" x2="150" y2="33"/><polyline points="0,28 25,28 50,20 75,20 100,11 125,11 150,3"/><circle cx="150" cy="3" r="2.5"/></svg>
<span class="mono">HBM3e · 4,1 TB/s</span></div>
</div>
<div class="grafico">
<p class="lead">Tokens por segundo y vatio, medidos con el mismo modelo de 8.000 millones de parámetros, cuantizado a 8 bits y lote 1. El S0 de 2024 fue nuestro primer silicio; la referencia es una GPU de centro de datos de la misma generación.</p>
<svg viewBox="0 0 640 240" role="img" aria-label="Gráfico de barras: S1 61,3; S0 27,8; GPU de referencia 19,4 tokens por segundo y vatio">
<g class="mono">
<text x="0" y="34">S1 · 3 nm · 2026</text><rect class="b" x="200" y="20" width="0" height="26" fill="#ffb000" data-w="430"/><text class="val" x="640" y="38" text-anchor="end">61,3</text>
<text x="0" y="114">S0 · 5 nm · 2024</text><rect class="b" x="200" y="100" width="0" height="26" fill="#8c93a3" data-w="195"/><text class="val" x="640" y="118" text-anchor="end">27,8</text>
<text x="0" y="194">GPU de referencia</text><rect class="b" x="200" y="180" width="0" height="26" fill="#3b4150" data-w="136"/><text class="val" x="640" y="198" text-anchor="end">19,4</text>
<line x1="200" y1="0" x2="200" y2="230" stroke="rgba(238,240,244,.2)"/>
</g>
</svg>
</div>
</section>
<!-- 5 · SERVIDOR -->
<section id="servidor" aria-label="Del silicio al centro de datos">
<div class="fijo">
<div class="poster" aria-hidden="true"><picture>
<source type="image/avif" srcset="" data-src="chip-macro-v1.avif" media="(min-width:900px)">
<source type="image/webp" srcset="" data-src="chip-macro-v1.webp" media="(min-width:900px)">
<img alt="" src="" data-src="chip-macro-1280-v1.webp" decoding="async"></picture></div>
<div class="srv-velo" aria-hidden="true"></div>
<h2 class="titular srv-t1"><span class="l">Del silicio</span></h2>
<p class="titular srv-t2" aria-hidden="true"><span class="l">al <em style="font-style:normal;color:var(--cian)">centro de datos.</em></span></p>
<p class="srv-p">Ocho S1 por nodo, 32 nodos por rack y refrigeración líquida con entrada a 45 °C. El encapsulado cae en su zócalo, los ventiladores arrancan y el calor se reparte por la placa.</p>
<div class="srv-hud mono">
<div><b id="srv-temp">41,0 °C</b>temperatura del die</div>
<div><b id="srv-rpm">0 rpm</b>ventiladores</div>
<div><b id="srv-w">0,0 kW</b>potencia del nodo</div>
</div>
</div>
</section>
<!-- 6 · NORVIK -->
<section id="norvik" aria-labelledby="mal-h">
<div>
<h2 class="sec" id="mal-h">Diseñado<br>en <em>Norvik.</em></h2>
<p class="lead" style="color:#4a5160">Ciento cuarenta personas entre el Parque Tecnológico de Norvik y un laboratorio de pruebas junto al puerto. Diseñamos aquí, fabricamos en Europa y probamos cada chip antes de enviarlo.</p>
<div class="dir">
<p class="mono"><b style="color:#0a0c10">Sede</b><br>C/ Marie Curie, 14 · 48210 Parkside (Norvik)</p>
<p class="mono"><b style="color:#0a0c10">Laboratorio</b><br>Muelle Norte, 3<br>48001 Norvik</p>
</div>
<ul class="empleo" id="empleo">
<li><a href="#kit"><span>Ingeniero/a de verificación RTL</span><span class="mono">PTN · híbrido</span><span class="fl">→</span></a></li>
<li><a href="#kit"><span>Diseño físico y cierre de tiempos</span><span class="mono">PTN · presencial</span><span class="fl">→</span></a></li>
<li><a href="#kit"><span>Compiladores para inferencia</span><span class="mono">remoto · Europa</span><span class="fl">→</span></a></li>
<li><a href="#kit"><span>Firmware y drivers PCIe</span><span class="mono">laboratorio · puerto</span><span class="fl">→</span></a></li>
</ul>
</div>
<div class="mapa" aria-hidden="true">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
<path class="mar dib" d="M0 205 C40 200 70 214 110 210 S180 190 230 196 S300 222 340 214 S390 196 400 198"/>
<path class="mar dib" d="M0 222 C40 218 70 232 110 228 S180 210 230 216 S300 240 340 232 S390 214 400 216" opacity=".5"/>
<path class="dib" d="M0 150 C50 140 80 120 120 110 S190 60 230 70 S290 40 340 30" opacity=".5"/>
<path class="dib" d="M40 300 C60 250 90 230 150 220 S250 150 300 120" opacity=".5"/>
<path class="dib" d="M0 178 L400 150" opacity=".35"/>
<path class="dib" d="M120 300 L150 180 L190 60" opacity=".35"/>
<path class="dib" d="M300 300 L280 200 L260 100" opacity=".35"/>
<line class="dib" x1="132" y1="118" x2="236" y2="192"/>
<circle class="dib" cx="132" cy="118" r="14"/><circle class="pta" cx="132" cy="118" r="5"/>
<text x="152" y="112">PTN · Parkside</text>
<circle class="dib" cx="236" cy="192" r="6"/><text x="246" y="186">Puerto · laboratorio</text>
<circle class="dib" cx="96" cy="176" r="4"/><text x="60" y="166">Aeropuerto</text>
<text x="300" y="262" class="mar" style="fill:#7f8fa8">Mar interior</text>
<text x="12" y="290">00° 00′ N · 00° 00′ E</text>
</svg>
</div>
</section>
<!-- 7 · KIT + FOOTER -->
<section id="kit" aria-labelledby="kit-h">
<div>
<h2 class="sec" id="kit-h">Pide acceso al<br><em>kit de desarrollo.</em></h2>
<p class="lead">Una tarjeta PCIe con un S1, el SDK con compilador y perfiles, y tres meses de soporte con el equipo de Norvik. Respondemos en 48 horas laborables.</p>
</div>
<form class="kit-form" id="kit-form" novalidate>
<div class="campo"><label for="k-emp">Empresa</label><input id="k-emp" type="text" required autocomplete="organization" placeholder="Nombre de la empresa"></div>
<div class="campo"><label for="k-mail">Email de trabajo</label><input id="k-mail" type="email" required autocomplete="email" placeholder="tu@empresa.com"></div>
<div class="campo"><label for="k-uso">Caso de uso</label>
<select id="k-uso"><option>Inferencia de modelos de lenguaje</option><option>Visión por computador</option><option>Sistemas de recomendación</option><option>Voz y transcripción</option></select></div>
<div class="campo"><label for="k-n">Chips estimados: <b id="k-nv">8</b></label><input id="k-n" type="range" min="1" max="512" value="8"></div>
<div class="calc">
<div><b id="c-tops">9.600</b><span class="mono">TOPS totales</span></div>
<div><b id="c-w">533 W</b><span class="mono">potencia</span></div>
<div><b id="c-rack">1 nodo</b><span class="mono">ocupación</span></div>
</div>
<button class="btn lleno" type="submit" style="justify-self:start">Solicitar acceso</button>
<p class="gracias mono" id="gracias">Solicitud recibida. Te escribimos en 48 h laborables.</p>
</form>
</section>
<footer aria-label="Pie de página">
<div class="poster" aria-hidden="true"><picture>
<source type="image/avif" srcset="" data-src="chip-macro-v1.avif" media="(min-width:900px)">
<source type="image/webp" srcset="" data-src="chip-macro-v1.webp" media="(min-width:900px)">
<img alt="" src="" data-src="chip-macro-1280-v1.webp" decoding="async"></picture></div>
<div class="marquesina" aria-hidden="true"><div>Silíceo · <em>S1</em> · 3 nm · Norvik · Silíceo · <em>S1</em> · 3 nm · Norvik · </div></div>
<div class="pie">
<div class="mono">Silíceo Semiconductores Ltd.<br>C/ Marie Curie, 14 · 48210 Parkside (Norvik)<br>© 2026 · <span id="pie-hora">--:--</span></div>
<ul class="mono"><li><a href="tel:+00000000000">+00 000 000 000</a></li><li><a href="mailto:hola@siliceo.com">hola@siliceo.com</a></li><li><a href="#kit">Kit de desarrollo</a></li></ul>
<ul class="mono"><li><a href="#zoom">Escala</a></li><li><a href="#servidor">Centro de datos</a></li><li><a href="#norvik">Empleo</a></li></ul>
</div>
</footer>
</main>
```
Notas sobre el contenido:
- Todos los textos están en español neutro. Las cifras no son redondas por casualidad: 562 chips (los que caben en una oblea de 300 mm con retícula de 10,5 mm y 3 mm de exclusión), 18,4 TOPS/W, 2,1 ms, 61,3 / 27,8 / 19,4 tokens por segundo y vatio, lote 26-0912.
- El HUD del hero muestra la hora real UTC (`toLocaleTimeString("es-ES", { timeZone: "UTC", hour12: false })`, cada segundo) y la inclinación real de la oblea en grados, leída del muelle.
- Los cinco «estados» del zoom (`#estados`) se generan desde el array `ETAPAS` y solo se muestran con movimiento reducido o sin WebGL.
## Mapa de escenas
Un solo reloj: `scrollY` movido por Lenis (`lerp: 0.09`) → `ScrollTrigger.update`. Cada escena lee el `progress` de su ScrollTrigger con `scrub: true`; la visibilidad se mide cada fotograma con `getBoundingClientRect()` de cada sección. El bucle dibuja una única escena por fotograma en el canvas fijo y limpia el lienzo donde ninguna aplica.
| Tramo | Sección (altura) | Objeto héroe | Cámara / shader | Texto |
|---|---|---|---|---|
| Intro 0–1,7 s | `#intro` (fijo) | La oblea aparece detrás cuando el GLB llega | Retícula ámbar que se proyecta; barra que baja en 1,15 s; contador 0 → 100 % | «SILÍCEO» por letras con desenfoque; «Exposición · retícula 26 × 33 mm · lote 26-0912» |
| Hero, progreso 0 → 1 | `#hero` (100 svh) | Oblea a la derecha (x 0,95, y 0,42), rotación base (−0,88, 0,38, −0,30), escala 0,84; muelle ±0,34 rad en X y ±0,42 en Y | Cámara fija en (0, 0,9, 8,1), fov 30°; entorno girando a 0,09 rad/s desde 0,9. Al salir el hero, `enc = h^1,4` encara la oblea (rotación → 0), la centra (y → 0,9) y la acerca (z → 3,2) | «Pensado» arriba-izquierda y «en silicio.» abajo-derecha, 14 vw, entrada por letras (y 46 → 0, blur 14 → 0, 0,9 s, power3.out, stagger 0,032) |
| Zoom q 0 → 0,18 | `#zoom` (520 svh, sticky) | Capa L0: disco con muesca y retícula de 562 chips | `uLog` de `lgInicial()` (el disco mide lo mismo que la oblea 3D encarada) a 1,6 | «300 mm» · «Oblea · 300 mm» |
| Zoom q 0,18 → 0,36 | | Capa L1: plano de planta del chip en cada die (textura) | `uLog` 1,6 → 0,1 | «10,5 mm» (ScrambleText con dígitos) |
| Zoom q 0,36 → 0,55 | | Capa L2: matrices SRAM y buses, paso 0,12 mm | `uLog` 0,1 → −0,9 | «1,2 mm» |
| Zoom q 0,55 → 0,70 | | Capa L3: baldosas Manhattan de cobre, vías, capa M1 tenue | `uLog` −0,9 → −1,5; **inundación ámbar** desde uLog −0,8 (`body.ambar-on`) | «4 µm» |
| Zoom q 0,70 → 0,80 | | Dentro de una pista (cobre uniforme) | `uLog` −1,5 → −2,95, tramo rápido | |
| Zoom q 0,80 → 1 | | Capa L4: aletas de 26 nm y puertas de 48 nm, contactos, transistor central resaltado en cian | `uLog` −2,95 → −3,75; el ámbar se apaga a −3,3 | «3 nm» |
| Inferencia | `#inferencia` (≥ 100 svh) | Fondo: capa L3 con `uPulsos` 1 mientras escribe (0,35 en reposo) | `uLog = −0,8 − 0,25·progreso` | Consola: respuesta por tokens, tok/s, primer token 21 ms |
| Métricas | `#metricas` | Loop de vídeo de la oblea (opacidad 0,75, `saturate(1.15) contrast(1.08)`, viñeta radial + degradado vertical encima) | — | 4 cifras que cuentan (1,8 s, power3.out), un micrográfico SVG bajo cada cifra que se traza (1,6 s) y barras SVG que crecen (1,4 s) |
| Servidor q 0 → 0,38 | `#servidor` (320 svh, sticky) | El S1 cae de y 2,4 a 0,068 rotando (−2,4, 1,6, 0,6) → (0, 0, 0), ease cúbica | Órbita: acimut π+0,75 → π+0,30, elevación 0,9 → 0,5, radio 9,4 | «Del silicio» / «al centro de datos.» por letras al entrar |
| Servidor q 0,36 → 0,61 | | Ventiladores hasta 42 rad/s; 24 LED cian en ola (q 0,42 → 0,72) | | HUD: rpm 0 → 9.800, potencia 0,3 → 2,4 kW |
| Servidor q 0,5 → 1 | | Mapa de calor `uCalor` 0 → 1 desde el zócalo A | Radio 9,4 → 5,6 y mira al zócalo A (q 0,55 → 1) | Temperatura 41,0 → 67,0 °C |
| Norvik | `#norvik` (claro) | Mapa SVG: trazos con `stroke-dashoffset` que se dibujan (1,8 s, stagger 0,08) | — | Ofertas de empleo entran (y 18 → 0, stagger 0,08) |
| Kit | `#kit` | Formulario que calcula | — | TOPS = n × 1.200; W = n × 65 + ⌈n/8⌉ × 180; nodos = ⌈n/8⌉ (racks a partir de 32 nodos) |
| Pie | `footer` (100 svh) | Render del encapsulado con parallax por profundidad (fuerza 0,028, foco 0,45) | `uScroll = (progreso − 1) × 0,5` | Marquesina de 11,5 vw (`clamp(56px, 11.5vw, 200px)`, `bottom: gutter + 78px`, para que el die respire) a 26 s por vuelta; debajo, pie de tres columnas en mono |
## Movimiento
| Elemento | Propiedad | De → a | Duración | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| `#intro .ret` | opacity | 0 → 1 | 0,5 s | power2.out | 0 | carga |
| `#intro .marca span` | opacity, filter, y | 0, blur 10, 0,3em → 1, 0, 0 | 0,7 s | power3.out | 0,1 s, stagger 0,05 | carga |
| `#intro .barra` | y | −4 → innerHeight + 8 | 1,15 s | power1.inOut | 0,15 s | carga |
| `#intro-pct` | texto | 0 % → 100 % | 1,15 s | none | 0,15 s | carga |
| `#intro .ret` | background-size, opacity | 3,85vw → 1,2vw; 1 → 0,35 | 0,5 s | power2.in | 1,0 s | carga |
| `#intro` | clip-path | inset(0) → inset(0 0 0 100%) | 0,7 s | power4.inOut | al acabar (≤ 2,4 s) | fin de intro o tecla/toque |
| Letras de `#t1` / `#t2` | opacity, y, filter | 0, 46 px, blur 14 → 1, 0, 0 | 0,9 s | power3.out | 0,1 s / 0,35 s, stagger 0,032 | fin de intro |
| `.hero-p` | opacity | 0 → 1 | 1 s | power1 | 0,7 s | fin de intro |
| Oblea (entrada) | scale | 0,62 → 1 (× 0,84) | ~1,33 s | easeOutQuart (`1 − (1−e)⁴`) | al cargar el GLB | GLB listo |
| Oblea (cursor) | rotation.x / rotation.y | objetivo = puntero.y × 0,34 / puntero.x × 0,42 | muelle k 170, c 26 | — | — | pointermove / arrastre / giroscopio |
| Oblea (auto) | objetivo | sin(t·0,5)·0,45, sin(t·0,31+1,2)·0,35 | continuo | — | 2,6 s sin puntero | inactividad |
| Entorno | environmentRotation.y | 0,9 + t × 0,09 | continuo | lineal | — | siempre (fijo con movimiento reducido) |
| `#zoom-num` | ScrambleText | cifra anterior → nueva | 0,6 s | chars «0123456789», speed 0,8 | — | cambio de etapa |
| `#zoom-uni` | ScrambleText | unidad | 0,5 s | chars «mµn», speed 0,6 | — | cambio de etapa |
| `#zoom-txt` | ScrambleText | frase de la etapa | 0,9 s | lowerCase, speed 1,2, revealDelay 0,1 | — | cambio de etapa |
| `.zoom-etapas span` | opacity | 0,35 → 1 (la activa) | 0,3 s | CSS | — | cambio de etapa |
| Puntero del zoom | uPuntero | ±0,018 × ancho | muelle k 60, c 14 | — | — | pointermove |
| `h2.sec` | opacity, y, filter | 0, 30, blur 8 → 1, 0, 0 | 0,9 s | power3.out | — | top 85 % |
| `[data-n]` | texto | 0 → valor | 1,8 s | power3.out | — | `#metricas` top 65 % (una vez) |
| `.grafico .b` | attr width | 0 → 430 / 195 / 136 | 1,4 s | power3.inOut | 0,2 s | `#metricas` top 65 % |
| Letras de `.srv-t1` / `.srv-t2` | como `#t1` | | 0,9 s | power3.out | 0 / 0,3 s | `#servidor` top 60 % |
| Chip | position.y, rotation | 2,4 → 0,068; (−2,4, 1,6, 0,6) → 0 | q 0 → 0,38 | `1 − (1−c)³` | — | scroll (scrub) |
| Ventiladores | rotation.z | += dt × vel × 42 | continuo | vel = clamp((q − 0,36)/0,25) | — | scroll |
| LED | color | #0a1014 → cian × (0,05 + 2,2·k·(0,75 + 0,25·sin(6t + i))) | ola q 0,42 → 0,72 | lineal por LED | — | scroll |
| Mapa de calor | uCalor | 0 → 1 | q 0,5 → 0,95 | lineal | — | scroll |
| Cámara del servidor | acimut, elevación, radio | ver mapa de escenas; puntero ±0,12 / ±0,06 con muelle k 120, c 20 | | | | scroll + puntero |
| `.mapa .dib` | stroke-dashoffset | longitud → 0 | 1,8 s | power2.inOut | stagger 0,08 | `#norvik` top 70 % |
| `.empleo li` | opacity, y | 0, 18 → 1, 0 | 0,7 s | power3.out | stagger 0,08 | `#norvik` top 70 % |
| `.marquesina div` | translateX | 0 → −50 % | 26 s | linear, infinito | — | CSS |
| Pie | uPuntero, uScroll | muelle k 50, c 12; (progreso − 1) × 0,5 | | | | puntero + scroll |
| Enlaces `#…` | scroll | Lenis `scrollTo(el)` | 1,4 s | `1 − (1−x)⁴` | — | clic |
| `.nav::before` (velo de la nav) | opacity | 0 → 1 (degradado negro + `backdrop-filter: blur(14px)` con máscara al 45 %) | 0,4 s | CSS | — | `body.nav-solida`: alguna sección DOM (`#inferencia`, `#metricas`, `#norvik`, `#kit`) toca la franja 0–72 px; nunca sobre las escenas WebGL |
| `.nav` | color | hueso → tinta (velo claro) | 0,4 s | CSS | — | `body.claro-on`: `#norvik` ocupa el píxel 40 |
| `.nav ul a` | ScrambleText | texto → el mismo texto | 0,55 s | chars upperCase, speed 1,1 | — | `pointerenter` (no con movimiento reducido) |
| `.btn::before` | scaleX | 0 → 1 desde la izquierda (relleno hueso, texto negro; ámbar en el tramo ámbar) | 0,5 s | `--ease-out` | — | hover / focus-visible |
| `.empleo .fl` | background, transform | transparente → tinta; x +4 px | 0,3 / 0,4 s | `--ease-out` | — | hover del enlace |
| `.cifra .mini polyline` | stroke-dashoffset | longitud → 0 | 1,6 s | power2.inOut | 0,3 s | `#metricas` top 65 % (el punto final aparece a 1,8 s) |
| Sombra de contacto del chip | opacity, scale | 0,15 → 0,85; 1,8 → 1,0 | q 0 → 0,38 | `1 − (1−c)³` | — | scroll (con la caída) |
## Código clave
Todo lo que sigue va **literal**. Es el módulo entero de la pieza, partido por bloques en el mismo orden en que aparece en el archivo.
### Constantes, ayudantes y reloj de scroll
```js
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
const ASSETS = "https://motionframe.app/assets/signature-siliceo"; // base de los recursos, sin barra final; no hay ninguna otra ruta a recursos en el archivo
const A = (n) => `${ASSETS}/${n}`;
const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
const COL = { ambar: css("--ambar") || "#ffb000", cian: css("--cian") || "#3be8ff", silicio: css("--silicio") || "#8c93a3" };
const reducido = matchMedia("(prefers-reduced-motion: reduce)").matches;
const tactil = matchMedia("(hover: none), (pointer: coarse)").matches;
const movil = () => innerWidth < 821;
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, t) => a + (b - a) * t;
const fmt = (n, d = 0) => { const [e, f] = Math.abs(n).toFixed(d).split("."); return (n < 0 ? "\u2212" : "") + e.replace(/\B(?=(\d{3})+(?!\d))/g, ".") + (f ? "," + f : ""); };
gsap.registerPlugin(ScrollTrigger, ScrambleTextPlugin);
if (!tactil) document.body.classList.add("puntero-ok");
// Los <picture> de respaldo apuntan a ASSETS (una sola constante para todas las rutas).
for (const el of $$("[data-src]")) { const u = A(el.dataset.src); if (el.tagName === "SOURCE") el.srcset = u; else el.src = u; }
// ---------- Reloj único de scroll: Lenis alimenta ScrollTrigger ----------
const lenis = new Lenis({ lerp: 0.09, wheelMultiplier: 1, smoothWheel: !reducido });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
for (const a of $$('a[href^="#"]')) a.addEventListener("click", (e) => {
const el = $(a.getAttribute("href")); if (!el) return; e.preventDefault(); lenis.scrollTo(el, { duration: 1.4, easing: (x) => 1 - Math.pow(1 - x, 4) });
});
```
### El muelle (único integrador de la pieza), el puntero y el cursor
```js
// ---------- Muelle (tensión 170, fricción 26): el único integrador de la pieza ----------
class Muelle {
constructor(k = 170, c = 26, x = 0) { this.k = k; this.c = c; this.x = x; this.v = 0; this.o = x; }
paso(dt) { const f = -this.k * (this.x - this.o) - this.c * this.v; this.v += f * dt; this.x += this.v * dt; return this.x; }
}
// ---------- Puntero, cursor y estado global ----------
const P = { x: 0, y: 0, nx: 0, ny: 0, activo: false, ultimo: 0 };
const cursorEl = $(".cursor");
addEventListener("pointermove", (e) => {
P.x = e.clientX; P.y = e.clientY; P.nx = e.clientX / innerWidth * 2 - 1; P.ny = e.clientY / innerHeight * 2 - 1;
P.activo = true; P.ultimo = performance.now();
cursorEl.style.transform = `translate(${P.x}px,${P.y}px) translate(-50%,-50%)`;
}, { passive: true });
addEventListener("pointerleave", () => { P.activo = false; });
```
### Detección de WebGL y modos estáticos
```js
// ---------- Soporte WebGL ----------
const canvas = $("#gl");
let renderer = null;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" });
} catch (e) { renderer = null; }
const hayGL = !!renderer && !!renderer.getContext();
if (!hayGL) { document.body.classList.add("no-webgl", "estatico"); canvas.remove(); }
if (reducido) document.body.classList.add("estatico");
```
### Titulares por letras
```js
// ---------- Textos: entrada por letras con desenfoque ----------
function partir(el) {
const l = el.querySelector(".l"); if (!l || l.dataset.ok) return [];
const nodos = [];
const recorrer = (n, dest) => {
for (const c of [...n.childNodes]) {
if (c.nodeType === 3) { for (const ch of c.textContent) { const s = document.createElement("span"); s.textContent = ch; dest.appendChild(s); if (ch !== " ") nodos.push(s); } }
else { const w = document.createElement(c.tagName.toLowerCase()); w.style.cssText = c.style.cssText; w.className = c.className; dest.appendChild(w); recorrer(c, w); }
}
};
const copia = document.createElement("span"); copia.className = "l"; recorrer(l, copia); l.replaceWith(copia); copia.dataset.ok = "1";
return nodos;
}
const letrasT1 = partir($("#t1")), letrasT2 = partir($("#t2"));
const letrasS1 = partir($(".srv-t1")), letrasS2 = partir($(".srv-t2"));
const entradaLetras = (letras, retardo = 0) => reducido ? gsap.set(letras, { opacity: 1 }) :
gsap.fromTo(letras, { opacity: 0, y: 46, filter: "blur(14px)" }, { opacity: 1, y: 0, filter: "blur(0px)", duration: 0.9, ease: "power3.out", stagger: 0.032, delay: retardo, overwrite: true });
gsap.set([...letrasT1, ...letrasT2, ...letrasS1, ...letrasS2], { opacity: 0 });
```
### Intro con concepto (≤ 2,4 s, se salta con tecla o toque)
```js
// ---------- Intro con concepto: la retícula de exposición revela la oblea (≤ 2,4 s, se salta con tecla o toque) ----------
const intro = $("#intro");
let introHecha = false;
function acabarIntro() {
if (introHecha) return; introHecha = true;
entradaLetras(letrasT1, 0.1); entradaLetras(letrasT2, 0.35);
gsap.to(".hero-p", { opacity: 1, duration: 1, delay: 0.7 });
if (ESC.oblea) ESC.oblea.entrada = 0;
gsap.to(intro, { clipPath: "inset(0 0 0 100%)", duration: 0.7, ease: "power4.inOut", onComplete: () => intro.remove() });
cursorEl.classList.add("visible");
}
if (reducido || !intro.isConnected) { intro.remove(); introHecha = true; gsap.set([...letrasT1, ...letrasT2], { opacity: 1 }); gsap.set(".hero-p", { opacity: 1 }); }
else {
const tl = gsap.timeline({ onComplete: acabarIntro });
tl.to("#intro .ret", { opacity: 1, duration: 0.5, ease: "power2.out" }, 0)
.to("#intro .marca span", { opacity: 1, filter: "blur(0px)", y: 0, duration: 0.7, stagger: 0.05, ease: "power3.out" }, 0.1)
.fromTo("#intro .barra", { y: -4 }, { y: innerHeight + 8, duration: 1.15, ease: "power1.inOut" }, 0.15)
.to({ v: 0 }, { v: 100, duration: 1.15, ease: "none", onUpdate() { $("#intro-pct").textContent = `${Math.round(this.targets()[0].v)} %`; } }, 0.15)
.to("#intro .ret", { backgroundSize: "1.2vw 1.2vw", opacity: 0.35, duration: 0.5, ease: "power2.in" }, 1.0);
const saltar = () => { if (!introHecha) { tl.kill(); acabarIntro(); } };
addEventListener("keydown", saltar, { once: true }); addEventListener("pointerdown", saltar, { once: true });
setTimeout(saltar, 2400);
}
```
### HUD en vivo (hora real UTC)
```js
// ---------- HUD en vivo ----------
const hora = () => new Date().toLocaleTimeString("es-ES", { timeZone: "UTC", hour12: false });
const ticHora = () => { $("#hud-hora").textContent = hora(); $("#pie-hora").textContent = hora().slice(0, 5); };
ticHora(); setInterval(ticHora, 1000);
// ---------- Escenas (una sola constante ASSETS, carga por prioridades) ----------
const ESC = { etapa: "hero", oblea: null, zoom: null, srv: null, pie: null, t: 0 };
const draco = new DRACOLoader().setDecoderPath("https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/libs/draco/gltf/");
const gltf = new GLTFLoader().setDRACOLoader(draco);
const texLoader = new THREE.TextureLoader();
const avifOk = new Promise((r) => { const i = new Image(); i.onload = () => r(i.width === 1); i.onerror = () => r(false);
i.src = "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A="; });
let W = innerWidth, H = innerHeight, DPR = 1;
if (hayGL) {
DPR = Math.min(devicePixelRatio || 1, movil() ? 1.5 : 2);
renderer.setPixelRatio(DPR);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x000000, 0);
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // un solo mapa (el foco del servidor)
}
```
### Escena A · la oblea (material físico, iridiscencia y difracción con `onBeforeCompile`)
```js
// ===== Escena A · la oblea =====
const uComun = { uTiempo: { value: 0 }, uDifr: { value: 1.6 }, uDifFreq: { value: 4.2 }, uAmbarCol: { value: new THREE.Color(COL.ambar) }, uCian: { value: new THREE.Color(COL.cian) } };
function crearOblea() {
const esc = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(30, W / H, 0.1, 60);
cam.position.set(0, 0.9, 8.1);
const pivote = new THREE.Group(); esc.add(pivote);
const pmrem = new THREE.PMREMGenerator(renderer);
const o = { esc, cam, pivote, listo: false, entrada: -1, sx: new Muelle(170, 26), sy: new Muelle(170, 26), sz: new Muelle(90, 18), env: null, hdrOk: false, flash: 0 };
// HDRI propio: la sala blanca. Si falla, un entorno mínimo hecho con luces.
new HDRLoader().load(A("sala-blanca-v1.hdr"), (hdr) => {
o.env = pmrem.fromEquirectangular(hdr).texture; hdr.dispose(); esc.environment = o.env; o.hdrOk = true;
esc.environmentIntensity = 1.35;
}, undefined, () => { const l = new THREE.DirectionalLight(0xffffff, 4); l.position.set(-2, 4, 3); esc.add(l, new THREE.AmbientLight(0x8c93a3, 0.6)); });
const normal = texLoader.load(A("chip-normal-v1.webp")); normal.wrapS = normal.wrapT = THREE.RepeatWrapping;
gltf.load(A("oblea-v1.glb"), (g) => {
g.scene.traverse((m) => {
if (!m.isMesh) return;
const esChip = /chip/i.test(m.material.name);
const mat = new THREE.MeshPhysicalMaterial({
color: esChip ? 0x5a6172 : 0x9aa1ae, metalness: esChip ? 0.72 : 0.92, roughness: esChip ? 0.17 : 0.07,
iridescence: esChip ? 1 : 0.4, iridescenceIOR: 1.45, iridescenceThicknessRange: [180, 760], envMapIntensity: 1.7,
clearcoat: esChip ? 0 : 0.6, clearcoatRoughness: 0.05,
});
if (esChip) { mat.normalMap = normal; mat.normalScale.set(0.42, 0.42); }
// Difracción de la retícula de chips: bandas de arcoíris que corren con la inclinación.
mat.onBeforeCompile = (sh) => {
Object.assign(sh.uniforms, uComun);
sh.fragmentShader = sh.fragmentShader
.replace("#include <common>", `#include <common>\nuniform float uTiempo, uDifr, uDifFreq; uniform vec3 uAmbarCol;`)
.replace("#include <opaque_fragment>", `
vec3 vDir = normalize(vViewPosition);
vec3 rfl = reflect(-vDir, normal);
float s1 = pow(max(0.0, dot(rfl, normalize(vec3(-0.35, 0.78, 0.5)))), 12.0);
float s2 = pow(max(0.0, dot(rfl, normalize(vec3(0.5, 0.62, 0.6)))), 16.0);
float s3 = pow(max(0.0, dot(rfl, normalize(vec3(-0.9, 0.2, 0.35)))), 12.0);
float s4 = pow(max(0.0, dot(rfl, normalize(vec3(0.1, 0.35, 0.93)))), 6.0);
float fase = (rfl.x * 0.9 + rfl.y * 0.35) * uDifFreq + uTiempo * 0.04;
vec3 arco = 0.5 + 0.5 * cos(6.2832 * (fase + vec3(0.0, 0.33, 0.67)));
float fres = pow(1.0 - max(0.0, dot(vDir, normal)), 2.2);
outgoingLight += arco * (s1 * 1.2 + s2 * 0.9 + s4 * 0.35) * uDifr * ${esChip ? "1.0" : "0.35"} * (0.3 + 0.7 * fres);
outgoingLight += uAmbarCol * s3 * 0.55 * ${esChip ? "1.0" : "0.5"};
#include <opaque_fragment>`);
};
m.material = mat;
});
g.scene.rotation.x = Math.PI / 2; // el disco pasa a mirar a +Z (cámara)
pivote.add(g.scene); o.listo = true; o.entrada = introHecha ? 0 : -1;
$("#hero .poster").style.opacity = "0";
}, undefined, () => document.body.classList.add("sin-glb"));
return o;
}
```
### Escena B · el zoom de 10⁸ (shader completo, vertex y fragment)
```js
// ===== Escena B · el zoom de 10⁸ (un solo ShaderMaterial a pantalla completa) =====
const ZOOM_FRAG = `
precision highp float;
uniform vec2 uRes; uniform float uLog, uTiempo, uPulsos, uAmbar, uMapaOk; uniform vec2 uPuntero;
uniform sampler2D uMapa; uniform vec3 uCian, uAmbarCol, uSilicio;
const float PITCH = 10.5; const float DIE = 9.7; const float RAD = 150.0;
const vec2 CENTRO = vec2(5.93, 6.22);
float hash21(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float ruido(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash21(i), hash21(i + vec2(1, 0)), f.x), mix(hash21(i + vec2(0, 1)), hash21(i + vec2(1, 1)), f.x), f.y); }
vec3 espectro(float t){ return 0.5 + 0.5 * cos(6.2832 * (t + vec3(0.0, 0.33, 0.67))); }
float caja(vec2 p, vec2 b){ vec2 d = abs(p) - b; return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); }
float luma(vec3 c){ return dot(c, vec3(0.299, 0.587, 0.114)); }
vec3 tinte(float t){
vec3 c = vec3(0.10, 0.11, 0.15);
c = mix(c, vec3(0.16, 0.20, 0.36), step(0.5, t) * step(t, 1.5));
c = mix(c, vec3(0.27, 0.17, 0.42), step(1.5, t) * step(t, 2.5));
c = mix(c, vec3(0.12, 0.30, 0.34), step(2.5, t) * step(t, 3.5));
c = mix(c, vec3(0.24, 0.25, 0.15), step(3.5, t) * step(t, 4.5));
c = mix(c, vec3(0.32, 0.17, 0.17), step(4.5, t) * step(t, 5.5));
c = mix(c, vec3(0.36, 0.38, 0.42), step(5.5, t));
return c;
}
// L0 · oblea: disco con muesca y retícula de chips
vec3 capaOblea(vec2 w, float aa, out float dentro, out float enDie, out vec2 loc){
float r = length(w);
float muesca = length(w - vec2(0.0, -RAD)) - 4.5;
float d = max(r - RAD, -muesca);
vec2 c = floor(w / PITCH); loc = w - (c + 0.5) * PITCH;
float dDie = caja(loc, vec2(DIE * 0.5));
enDie = 1.0 - smoothstep(-aa, aa, dDie);
float cabe = step(length((c + 0.5) * PITCH) + 7.4, RAD - 3.0);
enDie *= cabe;
float banda = sin(w.x * 0.05 + w.y * 0.018 + uTiempo * 0.25);
vec3 base = uSilicio * 0.28;
vec3 dieCol = uSilicio * 0.5 + 0.28 * espectro(0.55 + 0.14 * banda + w.y * 0.0012) * smoothstep(0.1, 1.0, banda) + 0.05;
vec3 col = mix(base, dieCol, enDie);
col += vec3(0.55) * smoothstep(3.0, 0.0, abs(r - RAD + 1.6)) * 0.6;
dentro = 1.0 - smoothstep(-aa, aa, d);
return col * dentro;
}
// L1 · el chip: plano de planta (textura propia) repetido en cada die
vec3 capaChip(vec2 loc, float aa, out float tipoOut){
vec2 uv = loc / DIE + 0.5;
vec4 m = texture2D(uMapa, uv);
float tipo = floor(m.g * 7.0 + 0.5); tipoOut = tipo;
vec3 col = tinte(tipo) * (0.55 + 0.7 * m.r) + m.b * vec3(0.55, 0.58, 0.66) * 0.9;
col = mix(vec3(0.06, 0.07, 0.09), col, uMapaOk);
return col;
}
// L2 · bloque de núcleos: matrices SRAM y buses (procedural, paso 0,12 mm)
vec3 capaBloque(vec2 w, float aa, float tipo){
float P = 0.12; vec2 g = w / P; vec2 c = floor(g); vec2 f = fract(g) - 0.5;
float h = hash21(c);
float dB = caja(f, vec2(0.43)); float bloque = 1.0 - smoothstep(-aa / P, aa / P, dB);
vec2 f2 = fract(g * 8.0) - 0.5; float bit = 1.0 - smoothstep(0.30, 0.34 + aa / P * 8.0, max(abs(f2.x), abs(f2.y)));
float sram = step(0.35, h);
float relieve = bloque * mix(0.35 + 0.5 * step(0.5, hash21(c + f2 * 0.0)) , 0.55 + 0.45 * bit, sram);
float bus = step(0.93, fract(g.y * 0.5 + 0.02)) + step(0.95, fract(g.x * 0.25 + 0.01));
vec3 col = mix(vec3(0.07, 0.08, 0.11), tinte(tipo) * 1.35 + vec3(0.06), relieve);
col += min(1.0, bus) * vec3(0.5, 0.53, 0.6) * 0.5;
col += (bit * bloque * sram - 0.5) * 0.05;
return col;
}
// L3 · enrutado: baldosas Manhattan (paso 4 µm, pista 0,44 µm), vías y pulsos cian
vec3 capaEnrutado(vec2 w, float aa, float t){
float P = 0.004; vec2 g = w / P; vec2 c = floor(g); vec2 f = fract(g) - 0.5;
float h = hash21(c);
float ancho = 0.11; float s = aa / P;
float lineH = 1.0 - smoothstep(ancho, ancho + s, abs(f.y));
float lineV = 1.0 - smoothstep(ancho, ancho + s, abs(f.x));
float lineL = max(lineH * step(f.x, ancho), lineV * step(f.y, ancho));
float tipo = floor(h * 4.0);
float m = tipo < 1.0 ? lineH : tipo < 2.0 ? lineV : tipo < 3.0 ? max(lineH, lineV) : lineL;
float via = (1.0 - smoothstep(0.09, 0.11 + s, length(f))) * step(0.55, hash21(c + 7.0));
float pul = 0.0, halo = 0.0;
if (uPulsos > 0.0) {
float fase = fract(g.x * 0.045 - t * 0.85 + hash21(vec2(c.y, 3.0)) * 3.0);
float pH = smoothstep(0.86, 1.0, fase) * step(tipo, 0.99);
float faseV = fract(g.y * 0.045 - t * 0.7 + hash21(vec2(c.x, 5.0)) * 3.0);
float pV = smoothstep(0.86, 1.0, faseV) * step(1.0, tipo) * step(tipo, 1.99);
pul = lineH * pH + lineV * pV;
halo = pH * (1.0 - smoothstep(0.0, 0.45, abs(f.y))) + pV * (1.0 - smoothstep(0.0, 0.45, abs(f.x)));
}
// capa inferior (M1) más fina y tenue, girada 90°, para densidad
vec2 g2 = w / (P * 0.25) + 0.5; vec2 c2 = floor(g2); vec2 f2 = fract(g2) - 0.5; float h2 = hash21(c2 + 11.0);
float l2 = h2 < 0.5 ? 1.0 - smoothstep(0.08, 0.08 + s * 2.0, abs(f2.x)) : 1.0 - smoothstep(0.08, 0.08 + s * 2.0, abs(f2.y));
float rim = lineH * smoothstep(ancho - 0.04, ancho, abs(f.y)) * step(f.y, 0.0) + lineV * smoothstep(ancho - 0.04, ancho, abs(f.x)) * step(0.0, f.x);
vec3 cobre = vec3(0.74, 0.47, 0.28);
vec3 col = vec3(0.05, 0.06, 0.08) + l2 * vec3(0.16, 0.14, 0.13) + m * cobre * 0.95 + via * vec3(0.92, 0.86, 0.7);
col += rim * m * vec3(0.5, 0.4, 0.3);
col += (pul * 1.7 + halo * 0.35) * uCian * uPulsos;
return col;
}
// L4 · transistores: aletas de 26 nm de paso y puertas de 48 nm
vec3 capaTransistor(vec2 w, float aa){
float pf = 2.6e-5, pg = 4.8e-5;
float fx = fract(w.x / pf) - 0.5; float gy = fract(w.y / pg) - 0.5;
float fin = 1.0 - smoothstep(0.14, 0.14 + aa / pf, abs(fx));
float gate = 1.0 - smoothstep(0.18, 0.18 + aa / pg, abs(gy));
float finShade = fin * (1.0 - abs(fx) / 0.14 * 0.55);
vec3 col = vec3(0.04, 0.05, 0.07) + finShade * vec3(0.18, 0.21, 0.29) * 1.5;
col += fin * (1.0 - smoothstep(0.0, 0.05, abs(abs(fx) - 0.12))) * 0.25;
float sombra = smoothstep(0.34, 0.2, abs(gy)) * 0.35;
col *= 1.0 - sombra;
vec3 oro = uAmbarCol * (0.5 + 0.5 * (1.0 - abs(gy) / 0.18)) * (0.85 + 0.15 * fin);
col = mix(col, oro, gate * 0.94);
float contacto = (1.0 - smoothstep(0.05, 0.07, length(vec2(fx, fract(w.y / pg + 0.5) - 0.5)))) * (1.0 - gate);
col += contacto * vec3(0.7, 0.72, 0.8);
float sel = smoothstep(7.0e-5, 2.5e-5, length(w - CENTRO));
col += sel * uCian * 0.35 * (0.7 + 0.3 * sin(uTiempo * 3.0));
return col;
}
float ventana(float lg, float hi, float lo){ return smoothstep(hi + 0.35, hi - 0.35, lg) * smoothstep(lo - 0.35, lo + 0.35, lg); }
void main(){
float lg = uLog; float Wmm = pow(10.0, lg);
vec2 p = (gl_FragCoord.xy - 0.5 * uRes) / uRes.x;
vec2 w = CENTRO + (p + uPuntero * 0.018) * Wmm;
float aa = Wmm / uRes.x;
float dentro, enDie, tipo; vec2 loc;
vec3 col = capaOblea(w, aa, dentro, enDie, loc);
float a1 = ventana(lg, 2.4, 0.2) * enDie;
vec3 c1 = capaChip(loc, aa, tipo);
col = mix(col, c1, a1);
float a2 = ventana(lg, 0.9, -1.2);
col = mix(col, capaBloque(w, aa, tipo), a2);
float a3 = ventana(lg, -0.5, -3.7);
col = mix(col, capaEnrutado(w, aa, uTiempo), a3);
float a4 = smoothstep(-2.6, -3.2, lg);
col = mix(col, capaTransistor(w, aa), a4);
// inundación ámbar: la fase de fotolitografía se ve en blanco y negro bajo luz amarilla
float l = luma(col);
vec3 ambarCol = uAmbarCol * (0.22 + 0.95 * l) + vec3(0.02);
col = mix(col, ambarCol, uAmbar);
// viñeta y grano
float v = smoothstep(1.25, 0.35, length(p * vec2(1.0, uRes.x / uRes.y)));
col *= mix(0.72, 1.0, v);
col += (hash21(gl_FragCoord.xy + fract(uTiempo) * 17.0) - 0.5) * 0.035;
gl_FragColor = vec4(col, 1.0);
}`;
function crearZoom() {
const uMapa = texLoader.load(A("chip-mapa-v1.webp"), () => { z.mat.uniforms.uMapaOk.value = 1; });
uMapa.wrapS = uMapa.wrapT = THREE.ClampToEdgeWrapping; uMapa.minFilter = THREE.LinearMipmapLinearFilter; uMapa.anisotropy = 4;
const mat = new THREE.ShaderMaterial({
uniforms: { uRes: { value: new THREE.Vector2(W * DPR, H * DPR) }, uLog: { value: 3.0 }, uTiempo: uComun.uTiempo, uPulsos: { value: 0 },
uAmbar: { value: 0 }, uMapaOk: { value: 0 }, uPuntero: { value: new THREE.Vector2() }, uMapa: { value: uMapa },
uCian: uComun.uCian, uAmbarCol: uComun.uAmbarCol, uSilicio: { value: new THREE.Color(COL.silicio) } },
vertexShader: `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`, fragmentShader: ZOOM_FRAG, depthTest: false, depthWrite: false,
});
const esc = new THREE.Scene(); esc.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat));
const z = { esc, cam: new THREE.Camera(), mat, px: new Muelle(60, 14), py: new Muelle(60, 14) };
return z;
}
```
### Escena C · el servidor, el encapsulado y el mapa de calor (shader completo)
```js
// ===== Escena C · del silicio al centro de datos =====
const CALOR_FRAG = `
uniform float uCalor, uTiempo; uniform vec3 uAmbarCol; varying vec2 vUv;
float hash21(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float ruido(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash21(i), hash21(i + vec2(1, 0)), f.x), mix(hash21(i + vec2(0, 1)), hash21(i + vec2(1, 1)), f.x), f.y); }
void main(){
vec2 d = vUv - vec2(0.26, 0.48); d.x *= 1.25;
float r = length(d);
float campo = smoothstep(0.62 * uCalor + 0.04, 0.0, r) * uCalor;
vec2 d2 = vUv - vec2(0.74, 0.48); d2.x *= 1.25;
campo += 0.55 * smoothstep(0.4 * uCalor + 0.03, 0.0, length(d2)) * uCalor;
campo += 0.16 * uCalor * (ruido(vUv * 7.0 + vec2(uTiempo * 0.15, 0.0)) - 0.5);
campo = clamp(campo, 0.0, 1.0);
vec3 col = mix(vec3(0.0), uAmbarCol, smoothstep(0.0, 0.55, campo));
col = mix(col, vec3(1.0, 0.96, 0.86), smoothstep(0.55, 1.0, campo));
float iso = abs(fract(campo * 9.0) - 0.5);
col += (1.0 - smoothstep(0.0, 0.07, iso)) * 0.18 * campo;
gl_FragColor = vec4(col, campo * 0.8);
}`;
// Textura procedural de la placa (máscara verde oscura, planos de masa, pistas, pads dorados y serigrafía blanca): el GLB solo trae color plano.
function texturaPlaca() {
const S = 1024, c = document.createElement("canvas"); c.width = c.height = S; const x = c.getContext("2d");
const rnd = (() => { let s = 26_0912; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; })();
x.fillStyle = "#0b2a1c"; x.fillRect(0, 0, S, S);
x.fillStyle = "#12382a"; // planos de masa
for (let i = 0; i < 26; i++) { const w = 60 + rnd() * 260, h = 40 + rnd() * 200; x.fillRect(rnd() * S, rnd() * S, w, h); }
x.strokeStyle = "#1e5a3c"; x.lineWidth = 2; // pistas Manhattan finas
for (let i = 0; i < 160; i++) { const a = rnd() * S, b = rnd() * S, l = 30 + rnd() * 220; x.beginPath(); x.moveTo(a, b); if (rnd() < 0.5) x.lineTo(a + l, b); else x.lineTo(a, b + l); x.stroke(); }
x.fillStyle = "#c9a24a"; // pads y vías doradas
for (let i = 0; i < 700; i++) { const r = 1.5 + rnd() * 2.5; x.beginPath(); x.arc(rnd() * S, rnd() * S, r, 0, 6.2832); x.fill(); }
x.strokeStyle = "rgba(238,240,244,.85)"; x.lineWidth = 2; x.font = "500 13px ui-monospace, Menlo, monospace"; x.fillStyle = "rgba(238,240,244,.85)";
for (let i = 0; i < 40; i++) { const w = 20 + rnd() * 60, h = 12 + rnd() * 40; x.strokeRect(rnd() * S, rnd() * S, w, h); } // siluetas de componentes
const rot = ["U12", "J4", "C118", "R77", "L9", "Q3", "SW1", "F2", "U31", "J7", "C204", "Y1"];
for (let i = 0; i < 48; i++) x.fillText(rot[i % rot.length], rnd() * S, rnd() * S);
for (const [u, v] of [[0.256, 0.5], [0.744, 0.5]]) { // marco de los zócalos A y B
x.lineWidth = 3; x.strokeRect(u * S - 88, v * S - 72, 176, 144);
x.font = "500 16px ui-monospace, Menlo, monospace"; x.fillText(u < 0.5 ? "ZOCALO A · S1" : "ZOCALO B · S1", u * S - 84, v * S - 80);
}
x.font = "500 22px ui-monospace, Menlo, monospace"; x.fillText("NODO S1-8 · REV C · NORVIK", 40, S - 40);
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; t.anisotropy = 4; t.wrapS = t.wrapT = THREE.RepeatWrapping; return t;
}
// Cepillado del aluminio: mapa de rugosidad con vetas horizontales (sin anisotropía real: sin tangentes en el GLB mete ruido).
function texturaCepillado() {
const S = 512, c = document.createElement("canvas"); c.width = c.height = S; const x = c.getContext("2d");
let s = 7; const rnd = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
x.fillStyle = "#8a8a8a"; x.fillRect(0, 0, S, S);
for (let i = 0; i < 2600; i++) { const v = 90 + rnd() * 90 | 0; x.strokeStyle = `rgb(${v},${v},${v})`; x.lineWidth = 0.6 + rnd() * 1.2; const y = rnd() * S; x.beginPath(); x.moveTo(0, y); x.lineTo(S, y + (rnd() - 0.5) * 1.5); x.stroke(); }
const t = new THREE.CanvasTexture(c); t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(3, 3); return t;
}
// Materiales PBR por nombre de material del GLB (el GLB trae solo color plano): aluminio cepillado, PCB con máscara y cobre.
const PBR_SRV = {
Chasis: { color: 0x9da2a9, metalness: 0.88, roughness: 0.42, cepillado: true },
Placa: { color: 0xffffff, metalness: 0, roughness: 0.46, clearcoat: 0.5, clearcoatRoughness: 0.28, mapa: true },
Zocalo: { color: 0x15161a, metalness: 0.05, roughness: 0.5 },
Aluminio: { color: 0x8e939a, metalness: 0.95, roughness: 0.36, cepillado: true },
DIMM: { color: 0x101114, metalness: 0.8, roughness: 0.36 },
Plastico: { color: 0x0c0d10, metalness: 0, roughness: 0.62 },
Cobre: { color: 0xc07a3c, metalness: 1, roughness: 0.32 },
};
function crearServidor() {
const esc = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(34, W / H, 0.05, 80);
const o = { esc, cam, grupo: new THREE.Group(), chip: null, ventiladores: [], leds: null, calor: null, sombra: null, listo: 0, q: 0, luz: null, sx: new Muelle(120, 20), sy: new Muelle(120, 20) };
esc.add(o.grupo);
if (ESC.oblea?.env) { esc.environment = ESC.oblea.env; esc.environmentIntensity = 0.9; }
// Foco cálido con el único mapa de sombras de la pieza (2048 en escritorio, 1024 en móvil); la luz no se mueve, el chip sí.
const luz = new THREE.SpotLight(0xfff4e0, 90, 40, 0.55, 0.6, 1.3); luz.position.set(-3, 7, -2); luz.target.position.set(-0.9, 0, -0.4); esc.add(luz, luz.target);
luz.castShadow = true; luz.shadow.mapSize.set(movil() ? 1024 : 2048, movil() ? 1024 : 2048); luz.shadow.bias = -0.0004; luz.shadow.normalBias = 0.02;
luz.shadow.camera.near = 2; luz.shadow.camera.far = 16; luz.shadow.radius = 4;
const luz2 = new THREE.PointLight(COL.cian, 6, 12, 1.6); luz2.position.set(0, 1.2, 3.2); esc.add(luz2);
esc.add(new THREE.AmbientLight(0x2a3040, 0.5), new THREE.HemisphereLight(0x8fa3c4, 0x0a0c10, 0.6));
o.luz = luz;
// suelo que solo recibe sombra: ancla el chasis al negro
const suelo = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.ShadowMaterial({ opacity: 0.55 }));
suelo.rotation.x = -Math.PI / 2; suelo.position.y = -0.002; suelo.receiveShadow = true; esc.add(suelo);
const placaTex = texturaPlaca(), cepTex = texturaCepillado();
const glbMat = (m) => {
if (!m.isMesh) return;
const p = PBR_SRV[m.material.name];
if (p) { const { mapa, cepillado, ...props } = p; m.material = new THREE.MeshPhysicalMaterial(props); if (mapa) m.material.map = placaTex; if (cepillado) m.material.roughnessMap = cepTex; }
m.material.envMapIntensity = 1.1; m.castShadow = true; m.receiveShadow = true;
};
gltf.load(A("servidor-v1.glb"), (g) => {
g.scene.traverse((m) => { glbMat(m); if (/^Ventilador_\d/.test(m.name)) o.ventiladores.push(m); });
o.grupo.add(g.scene);
// LED de estado en el frontal: 24 puntos cian que se encienden en ola
const led = new THREE.InstancedMesh(new THREE.BoxGeometry(0.05, 0.03, 0.02), new THREE.MeshBasicMaterial({ color: 0x0a1014 }), 24);
const mtx = new THREE.Matrix4();
for (let i = 0; i < 24; i++) { mtx.setPosition(-1.55 + i * 0.135, 0.41, 3.49); led.setMatrixAt(i, mtx); led.setColorAt(i, new THREE.Color(0x0a1014)); }
led.instanceColor.needsUpdate = true; o.leds = led; o.grupo.add(led);
// mapa de calor sobre la placa
const calor = new THREE.Mesh(new THREE.PlaneGeometry(3.9, 4.8), new THREE.ShaderMaterial({
uniforms: { uCalor: { value: 0 }, uTiempo: uComun.uTiempo, uAmbarCol: uComun.uAmbarCol },
vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: CALOR_FRAG, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
calor.rotation.x = -Math.PI / 2; calor.position.set(0, 0.075, -0.55); o.calor = calor; o.grupo.add(calor);
o.listo++;
});
gltf.load(A("chip-v1.glb"), (g) => {
const chip = new THREE.Group();
let bola = null;
g.scene.traverse((m) => { if (m.isMesh) { m.material.envMapIntensity = 1.1; m.castShadow = true; m.receiveShadow = true; if (m.name === "Bola") bola = m; if (/Die/.test(m.name)) { m.material.iridescence = 1; m.material.iridescenceIOR = 1.3; m.material.iridescenceThicknessRange = [250, 800]; } } });
if (bola) {
bola.visible = false;
if (!movil()) {
const n = 30, L = 0.48, paso = L / (n - 1); const inst = new THREE.InstancedMesh(bola.geometry, bola.material, n * n);
const m = new THREE.Matrix4(); let k = 0;
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) { if (Math.abs(i - 14.5) < 4 && Math.abs(j - 14.5) < 4) continue; m.setPosition(-L / 2 + i * paso, -0.0045, -L / 2 + j * paso); inst.setMatrixAt(k++, m); }
inst.count = k; chip.add(inst);
}
}
chip.add(g.scene); o.chip = chip; o.grupo.add(chip); o.listo++;
});
// sombra de contacto bajo el chip: disco radial que se concentra al encajar (la sombra dura la pone el foco)
const sc = document.createElement("canvas"); sc.width = sc.height = 128; const sx = sc.getContext("2d");
const gr = sx.createRadialGradient(64, 64, 8, 64, 64, 64); gr.addColorStop(0, "rgba(0,0,0,.9)"); gr.addColorStop(0.55, "rgba(0,0,0,.35)"); gr.addColorStop(1, "rgba(0,0,0,0)");
sx.fillStyle = gr; sx.fillRect(0, 0, 128, 128);
const sombra = new THREE.Mesh(new THREE.PlaneGeometry(0.9, 0.9), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(sc), transparent: true, depthWrite: false, opacity: 0 }));
sombra.rotation.x = -Math.PI / 2; sombra.position.set(-0.95, 0.0975, -0.4); o.sombra = sombra; o.grupo.add(sombra);
return o;
}
```
### Escena D · el pie con parallax por profundidad (shader completo)
```js
// ===== Escena D · pie: render del encapsulado con parallax por profundidad =====
function crearPie() {
const u = { uImg: { value: null }, uDep: { value: null }, uPuntero: { value: new THREE.Vector2() }, uAspecto: { value: W / H }, uFuerza: { value: 0.028 }, uFoco: { value: 0.45 }, uScroll: { value: 0 }, uOk: { value: 0 } };
avifOk.then((ok) => {
const img = texLoader.load(A(ok ? "chip-macro-v1.avif" : (movil() ? "chip-macro-1280-v1.webp" : "chip-macro-v1.webp")), () => { u.uOk.value = 1; });
img.colorSpace = THREE.SRGBColorSpace; img.minFilter = THREE.LinearFilter; img.generateMipmaps = false;
const dep = texLoader.load(A("chip-macro-profundidad-v1.webp")); dep.minFilter = THREE.LinearFilter; dep.generateMipmaps = false;
u.uImg.value = img; u.uDep.value = dep;
});
const mat = new THREE.ShaderMaterial({ uniforms: u, depthTest: false, depthWrite: false,
vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`,
fragmentShader: `
uniform sampler2D uImg, uDep; uniform vec2 uPuntero; uniform float uAspecto, uFuerza, uFoco, uScroll, uOk; varying vec2 vUv;
void main(){
vec2 uv = vUv - 0.5; float ai = 16.0 / 9.0;
if (uAspecto > ai) uv.y *= ai / uAspecto; else uv.x *= uAspecto / ai;
uv = uv * 0.94 + 0.5;
vec2 m = uPuntero * vec2(1.0, -1.0) + vec2(0.0, uScroll);
vec2 p = uv;
for (int i = 0; i < 6; i++) { float d = texture2D(uDep, p).r; p = uv - m * (d - uFoco) * uFuerza; }
vec3 c = texture2D(uImg, p).rgb * uOk;
float v = smoothstep(1.2, 0.3, length(vUv - 0.5));
gl_FragColor = vec4(c * mix(0.3, 0.68, v) * (0.55 + 0.45 * smoothstep(0.0, 0.55, vUv.y)), 1.0);
#include <colorspace_fragment>
}` });
const esc = new THREE.Scene(); esc.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat));
return { esc, cam: new THREE.Camera(), u, px: new Muelle(50, 12), py: new Muelle(50, 12) };
}
```
### Carga por prioridades, ScrollTriggers y disparadores del DOM
```js
// ---------- Carga por prioridades: hero primero, el resto cuando el hilo está libre ----------
if (hayGL) {
ESC.oblea = crearOblea();
const idle = window.requestIdleCallback ? (f) => requestIdleCallback(f, { timeout: 1200 }) : (f) => setTimeout(f, 800);
idle(() => { ESC.zoom = crearZoom(); });
idle(() => { ESC.srv = crearServidor(); ESC.pie = crearPie(); });
}
// ---------- Progresos de scroll (ScrollTrigger con scrub sobre el mismo reloj) ----------
const PR = { hero: 0, zoom: 0, inf: 0, srv: 0, pie: 0, zoomVisible: false, srvVisible: false, pieVisible: false, infVisible: false, heroVisible: true };
ScrollTrigger.create({ trigger: "#hero", start: "top top", end: "bottom top", scrub: true, onUpdate: (s) => { PR.hero = s.progress; }, onToggle: (s) => { cursorEl.classList.toggle("visible", s.isActive && introHecha); } });
ScrollTrigger.create({ trigger: "#zoom", start: "top top", end: "bottom bottom", scrub: true, onUpdate: (s) => { PR.zoom = s.progress; } });
ScrollTrigger.create({ trigger: "#inferencia", start: "top bottom", end: "bottom top", scrub: true, onUpdate: (s) => { PR.inf = s.progress; } });
ScrollTrigger.create({ trigger: "#servidor", start: "top top", end: "bottom bottom", scrub: true, onUpdate: (s) => { PR.srv = s.progress; } });
ScrollTrigger.create({ trigger: "footer", start: "top bottom", end: "bottom bottom", scrub: true, onUpdate: (s) => { PR.pie = s.progress; } });
const SEC = { hero: $("#hero"), zoom: $("#zoom"), inf: $("#inferencia"), srv: $("#servidor"), pie: $("footer") };
const visible = (el) => { const r = el.getBoundingClientRect(); return r.top < H && r.bottom > 0; };
function medirVisibles() { PR.heroVisible = visible(SEC.hero); PR.zoomVisible = visible(SEC.zoom); PR.infVisible = visible(SEC.inf); PR.srvVisible = visible(SEC.srv); PR.pieVisible = visible(SEC.pie); }
// Titulares del servidor entran por letras al llegar; el hint del hero se descifra
ScrollTrigger.create({ trigger: "#servidor", start: "top 60%", once: true, onEnter: () => { entradaLetras(letrasS1, 0); entradaLetras(letrasS2, 0.3); } });
// Cifras que cuentan y gráfico que se dibuja
ScrollTrigger.create({ trigger: "#metricas", start: "top 65%", once: true, onEnter: () => {
for (const s of $$("[data-n]")) { const n = +s.dataset.n, d = s.dataset.f === "dec1" ? 1 : 0; gsap.to({ v: 0 }, { v: n, duration: reducido ? 0 : 1.8, ease: "power3.out", onUpdate() { s.textContent = fmt(this.targets()[0].v, d); } }); }
for (const b of $$(".grafico .b")) gsap.to(b, { attr: { width: +b.dataset.w }, duration: reducido ? 0 : 1.4, ease: "power3.inOut", delay: 0.2 });
for (const p of $$(".cifra .mini polyline")) { const L = p.getTotalLength(); gsap.fromTo(p, { strokeDasharray: L, strokeDashoffset: L }, { strokeDashoffset: 0, duration: reducido ? 0 : 1.6, ease: "power2.inOut", delay: 0.3 }); }
gsap.fromTo(".cifra .mini circle", { opacity: 0 }, { opacity: 1, duration: 0.3, delay: reducido ? 0 : 1.8 });
} });
// Mapa de Norvik: líneas que se dibujan
for (const p of $$(".mapa .dib")) { const L = p.getTotalLength ? p.getTotalLength() : 600; p.style.strokeDasharray = L; p.style.strokeDashoffset = L; }
ScrollTrigger.create({ trigger: "#norvik", start: "top 70%", once: true, onEnter: () => {
gsap.to(".mapa .dib", { strokeDashoffset: 0, duration: reducido ? 0 : 1.8, ease: "power2.inOut", stagger: 0.08 });
gsap.from(".empleo li", { opacity: 0, y: 18, duration: reducido ? 0 : 0.7, stagger: 0.08, ease: "power3.out" });
} });
// Secciones DOM: entrada de titulares
for (const h of $$("h2.sec")) ScrollTrigger.create({ trigger: h, start: "top 85%", once: true, onEnter: () => gsap.from(h, { opacity: 0, y: 30, filter: "blur(8px)", duration: reducido ? 0 : 0.9, ease: "power3.out" }) });
// Nav: los enlaces se descifran al pasar (ancho fijado para que no salten) y el velo solo aparece sobre las secciones DOM
for (const a of $$(".nav ul a")) {
a.dataset.t = a.textContent;
if (!reducido) a.addEventListener("pointerenter", () => gsap.to(a, { duration: 0.55, scrambleText: { text: a.dataset.t, chars: "upperCase", speed: 1.1 }, overwrite: true }));
}
(document.fonts?.ready || Promise.resolve()).then(() => { for (const a of $$(".nav ul a")) a.style.setProperty("--w", `${a.offsetWidth}px`); });
const SEC_DOM = $$("#inferencia, #metricas, #norvik, #kit"), secNorvik = $("#norvik");
function navEstado() {
const r = secNorvik.getBoundingClientRect();
document.body.classList.toggle("claro-on", r.top < 40 && r.bottom > 40);
document.body.classList.toggle("nav-solida", SEC_DOM.some((s) => { const q = s.getBoundingClientRect(); return q.top < 72 && q.bottom > 0; }));
}
// Vídeo de fondo de métricas: se carga solo cuando se acerca
const vid = $("#metricas video");
ScrollTrigger.create({ trigger: "#metricas", start: "top 150%", once: true, onEnter: () => {
if (reducido) { const img = document.createElement("img"); img.className = "loop-poster"; img.alt = ""; img.src = A(vid.dataset.poster); vid.replaceWith(img); return; }
vid.poster = A(vid.dataset.poster);
for (const [t, k] of [["video/webm", "webm"], ["video/mp4", "mp4"]]) { const s = document.createElement("source"); s.type = t; s.src = A(vid.dataset[k]); vid.appendChild(s); }
vid.load(); vid.play().catch(() => {});
} });
ScrollTrigger.create({ trigger: "#metricas", start: "top bottom", end: "bottom top", onToggle: (s) => { if (!vid.isConnected) return; s.isActive ? vid.play().catch(() => {}) : vid.pause(); } });
```
### Escala: etapas, barra de escala, aumento y estados estáticos
```js
// ---------- Escala: etiquetas, barra y aumento ----------
const ETAPAS = [
{ lg: 3.0, n: "300", u: "mm", t: "Una oblea de 300 mm con 562 chips S1. Cada uno es un acelerador completo, probado antes del corte." },
{ lg: 1.6, n: "10,5", u: "mm", t: "El chip: 8 núcleos tensoriales, 48 MB de caché y cuatro pilas de HBM3e a los flancos, en 26 × 33 mm de retícula." },
{ lg: 0.3, n: "1,2", u: "mm", t: "Un bloque de núcleos: matrices SRAM y buses de 512 bits. Aquí viven los 48 MB de caché." },
{ lg: -1.0, n: "4", u: "µm", t: "El enrutado: 17 capas de cobre con pistas de 0,44 µm, expuestas con litografía EUV. Por aquí viajan los pulsos de la inferencia." },
{ lg: -3.5, n: "3", u: "nm", t: "El transistor: aletas de silicio de 26 nm de paso y puertas de 48 nm. Hay 128.000 millones en cada S1." },
];
const LG0 = 3.0, LG1 = -3.75;
// Tramos del zoom (progreso → log10 del ancho de pantalla en mm): oblea 0→0,18 · chip 0,18→0,36 · bloque 0,36→0,55 ·
// enrutado 0,55→0,7 · dentro del cable (ámbar) 0,7→0,8 · transistor 0,8→1
// El primer tramo se calcula en tam(): el disco del shader debe medir lo mismo que la oblea 3D encarada (cámara a 4,9 u, fov 30°).
const lgInicial = () => Math.log10(300 * W / (3 * (movil() ? 0.62 : 0.84) / 2.626 * H));
const TRAMOS = [[0, 2.7], [0.18, 1.6], [0.36, 0.1], [0.55, -0.9], [0.7, -1.5], [0.8, -2.95], [1, -3.75]];
function logDeProgreso(q) {
for (let i = 1; i < TRAMOS.length; i++) if (q <= TRAMOS[i][0]) { const [q0, l0] = TRAMOS[i - 1], [q1, l1] = TRAMOS[i]; return lerp(l0, l1, (q - q0) / (q1 - q0)); }
return LG1;
}
let etapaAct = -1;
function barraEscala(Wmm) {
const bw = $(".barra-escala").offsetWidth / innerWidth; let v = Wmm * bw, u = "mm";
if (v < 1e-3) { v *= 1e6; u = "nm"; } else if (v < 1) { v *= 1e3; u = "µm"; }
const p = Math.pow(10, Math.floor(Math.log10(v))); const r = Math.round(v / p * 2) / 2 * p;
return `${fmt(r, r < 10 ? 1 : 0)} ${u}`;
}
const zoomNum = $("#zoom-num"), zoomUni = $("#zoom-uni"), zoomTxt = $("#zoom-txt"), barraV = $("#barra-v"), zoomAum = $("#zoom-aum"), etapasEl = $$(".zoom-etapas span");
function actualizarZoomHUD(lg) {
const Wmm = Math.pow(10, lg);
barraV.textContent = barraEscala(Wmm);
const aum = 1000 / Wmm; const ex = Math.floor(Math.log10(aum)); const mant = aum / Math.pow(10, ex);
zoomAum.textContent = aum < 10 ? `×${fmt(aum, 1)}` : `×${fmt(mant, 1)}·10${String(ex).replace(/\d/g, (d) => "⁰¹²³⁴⁵⁶⁷⁸⁹"[d])}`;
let e = 0; for (let i = 0; i < ETAPAS.length; i++) if (lg <= ETAPAS[i].lg + 0.45) e = i;
if (e !== etapaAct) {
etapaAct = e; const E = ETAPAS[e];
etapasEl.forEach((s, i) => s.classList.toggle("on", i === e));
if (reducido) { zoomNum.textContent = E.n; zoomUni.textContent = ` ${E.u}`; zoomTxt.textContent = E.t; }
else {
gsap.to(zoomNum, { duration: 0.6, scrambleText: { text: E.n, chars: "0123456789", speed: 0.8 }, overwrite: true });
gsap.to(zoomUni, { duration: 0.5, scrambleText: { text: ` ${E.u}`, chars: "mµn", speed: 0.6 }, overwrite: true });
gsap.to(zoomTxt, { duration: 0.9, scrambleText: { text: E.t, chars: "lowerCase", speed: 1.2, revealDelay: 0.1 }, overwrite: true });
}
}
const ambar = (lg < -0.8 && lg > -3.3) ? Math.min(clamp((-0.8 - lg) / 0.4, 0, 1), clamp((lg + 3.3) / 0.4, 0, 1)) : 0;
document.body.classList.toggle("ambar-on", ambar > 0.5);
return ambar;
}
// Estados estáticos (movimiento reducido / sin WebGL): una sucesión de estados finales
$("#estados").innerHTML = ETAPAS.map((E, i) => `<div class="estado"><div><h3>${E.n} <small style="font-size:.4em">${E.u}</small></h3><p class="lead">${E.t}</p></div>
<div class="cuadro" style="background:${["radial-gradient(circle at 50% 50%,#3a4050 0,#1d222c 58%,#050608 60%)", "repeating-linear-gradient(0deg,#1c2238 0 8%,#0b0d12 8% 10%),repeating-linear-gradient(90deg,#26305a 0 12%,transparent 12% 14%)", "repeating-linear-gradient(90deg,#1a2038 0 6px,#0b0d12 6px 8px)", "repeating-linear-gradient(0deg,#5c3a1c 0 3px,#ffb000 3px 4px,#3a2a10 4px 12px)", "repeating-linear-gradient(90deg,#2a3242 0 5px,#0b0d12 5px 13px),repeating-linear-gradient(0deg,rgba(255,176,0,.55) 0 5px,transparent 5px 22px)"][i]};background-blend-mode:screen"></div></div>`).join("");
```
### Inferencia simulada
```js
// ---------- Inferencia simulada ----------
const RESP = {
reposo: "En reposo, con los ocho núcleos en puerta cerrada y la HBM en autorrefresco, un S1 consume 4,2 W. Al primer token sube a 58 W y se estabiliza en 65 W de TDP. El arranque completo desde reposo tarda 9 ms.",
defecto: "El S1 es un acelerador de inferencia de 3 nm con 1.200 TOPS INT8, 96 GB de HBM3e y 4,1 TB/s de ancho de banda. Cabe en una tarjeta PCIe de una ranura, consume 65 W y se programa con un compilador abierto que acepta grafos estándar. Para un modelo de 8.000 millones de parámetros genera más de 1.100 tokens por segundo con lote 1.",
latencia: "La latencia p99 con un modelo de 8.000 millones de parámetros, cuantizado a 8 bits y lote 1, es de 2,1 ms por token. El primer token llega en 21 ms, incluida la carga del contexto.",
precio: "El kit de desarrollo incluye una tarjeta PCIe con un S1, el SDK y tres meses de soporte. El precio se comunica al aprobar la solicitud; el volumen se negocia por nodo de ocho chips.",
};
const elegir = (q) => /repos|consum|vatio|potencia/i.test(q) ? RESP.reposo : /latenc|rápid|rapid|tiempo|ms\b/i.test(q) ? RESP.latencia : /precio|cuesta|coste|comprar/i.test(q) ? RESP.precio : RESP.defecto;
const INF = { activo: false, tokens: [], i: 0, t0: 0, ult: 0, acumT: 0, acumN: 0 };
const infOut = $("#inf-out"), tokS = $("#tok-s"), tokN = $("#tok-n"), tokLat = $("#tok-lat");
function inferir(q) {
const texto = elegir(q); INF.tokens = texto.match(/\S+\s*|\s+/g) || [texto]; INF.i = 0; INF.activo = true; INF.t0 = performance.now();
INF.ult = INF.t0 + 21; INF.acumT = 0; INF.acumN = 0;
infOut.innerHTML = '<span class="c"></span>'; tokN.textContent = `0 / ${INF.tokens.length}`; tokLat.textContent = "21 ms"; tokS.textContent = "0";
}
function pasoInferencia(now) {
if (!INF.activo) return;
if (now < INF.ult) return;
const tok = INF.tokens[INF.i++];
infOut.insertBefore(document.createTextNode(tok), infOut.lastChild);
INF.acumN++; const dt = now - INF.ult; INF.acumT += dt;
// cadencia ~1.100 tok/s con jitter; visible: se muestra despacio para leer (un token cada 26–60 ms) pero la cifra refleja el ritmo real del chip
const tps = 980 + Math.sin(now / 190) * 160 + (Math.random() - 0.5) * 90;
tokS.textContent = fmt(Math.round(tps)); tokN.textContent = `${INF.i} / ${INF.tokens.length}`;
INF.ult = now + (reducido ? 0 : 26 + Math.random() * 34);
if (INF.i >= INF.tokens.length) { INF.activo = false; infOut.lastChild?.remove(); }
}
$("#inf-form").addEventListener("submit", (e) => { e.preventDefault(); inferir($("#inf-q").value.trim() || "S1"); });
ScrollTrigger.create({ trigger: "#inferencia", start: "top 55%", once: true, onEnter: () => inferir($("#inf-q").value) });
```
### Formulario que calcula
```js
// ---------- Formulario que calcula ----------
const kn = $("#k-n"), knv = $("#k-nv"), cT = $("#c-tops"), cW = $("#c-w"), cR = $("#c-rack");
function calc() {
const n = +kn.value; knv.textContent = n; cT.textContent = fmt(n * 1200); const w = n * 65 + Math.ceil(n / 8) * 180;
cW.textContent = w >= 1000 ? `${fmt(w / 1000, 1)} kW` : `${fmt(w)} W`;
const nodos = Math.ceil(n / 8); cR.textContent = nodos <= 1 ? "1 nodo" : nodos <= 32 ? `${nodos} nodos` : `${fmt(nodos / 32, 1)} racks`;
}
kn.addEventListener("input", calc); calc();
$("#kit-form").addEventListener("submit", (e) => {
e.preventDefault(); const f = e.target; const emp = $("#k-emp"), mail = $("#k-mail");
if (!emp.value.trim()) { emp.focus(); emp.setAttribute("aria-invalid", "true"); return; }
if (!mail.checkValidity()) { mail.focus(); mail.setAttribute("aria-invalid", "true"); return; }
$("#gracias").style.display = "block"; f.querySelector("button").disabled = true;
});
```
### Táctil: arrastre, giroscopio y vaivén
```js
// ---------- Táctil: arrastre gira la oblea; giroscopio si lo hay; si no, vaivén automático ----------
const T = { arrastra: false, x0: 0, y0: 0, dx: 0, dy: 0, giro: false };
if (tactil) {
const hero = $("#hero");
hero.addEventListener("touchstart", (e) => { T.arrastra = true; T.x0 = e.touches[0].clientX - T.dx * 200; T.y0 = e.touches[0].clientY - T.dy * 200; }, { passive: true });
hero.addEventListener("touchmove", (e) => { if (!T.arrastra) return; T.dx = clamp((e.touches[0].clientX - T.x0) / 200, -1, 1); T.dy = clamp((e.touches[0].clientY - T.y0) / 200, -1, 1); P.ultimo = performance.now(); }, { passive: true });
hero.addEventListener("touchend", () => { T.arrastra = false; });
addEventListener("deviceorientation", (e) => { if (e.gamma == null) return; T.giro = true; T.dx = clamp(e.gamma / 30, -1, 1); T.dy = clamp((e.beta - 40) / 30, -1, 1); P.ultimo = performance.now(); }, { passive: true });
}
```
### Bucle de dibujo (una escena por fotograma)
```js
// ---------- Bucle de dibujo (una escena por fotograma; se para fuera de pantalla y con la pestaña oculta) ----------
let necesita = true;
function tam() {
W = innerWidth; H = innerHeight;
if (!hayGL) return;
renderer.setSize(W, H, false);
for (const o of [ESC.oblea, ESC.srv]) if (o) { o.cam.aspect = W / H; o.cam.updateProjectionMatrix(); }
if (ESC.zoom) ESC.zoom.mat.uniforms.uRes.value.set(W * DPR, H * DPR);
if (ESC.pie) ESC.pie.u.uAspecto.value = W / H;
TRAMOS[0][1] = lgInicial();
necesita = true;
}
addEventListener("resize", () => { tam(); ScrollTrigger.refresh(); });
tam();
let ultimoT = performance.now();
const hudTilt = $("#hud-tilt"), srvTemp = $("#srv-temp"), srvRpm = $("#srv-rpm"), srvW = $("#srv-w");
const tmp = new THREE.Vector3();
function fotograma(now) {
const dt = Math.min((now - ultimoT) / 1000, 1 / 30); ultimoT = now; ESC.t += dt;
uComun.uTiempo.value = ESC.t;
pasoInferencia(now); navEstado();
if (!hayGL || document.hidden) return;
medirVisibles();
const O = ESC.oblea;
const quieto = reducido;
// --- objetivo del muelle: cursor, arrastre, giroscopio o vaivén automático
const auto = (now - P.ultimo) > 2600 || (tactil && !T.arrastra && !T.giro);
let ox = 0, oy = 0;
if (auto) { ox = Math.sin(ESC.t * 0.5) * 0.45; oy = Math.sin(ESC.t * 0.31 + 1.2) * 0.35; }
else if (tactil) { ox = T.dx; oy = T.dy; }
else { ox = P.nx; oy = P.ny; }
if (quieto) { ox = 0.25; oy = -0.15; }
if (PR.heroVisible) {
if (O?.listo) {
O.sx.o = oy * 0.34; O.sy.o = ox * 0.42; O.sx.paso(dt); O.sy.paso(dt);
if (O.entrada >= 0 && O.entrada < 1) O.entrada = Math.min(1, O.entrada + dt * 0.75);
const e = O.entrada < 0 ? 0 : O.entrada, k = 1 - Math.pow(1 - e, 4);
const h = PR.hero; // 0 → 1 mientras el hero sale: la oblea se encara y crece hacia el zoom
const enc = h < 0.001 ? 0 : Math.pow(h, 1.4);
const baseX = lerp(-0.88, 0.0, enc), baseY = lerp(0.38, 0.0, enc), baseZ = lerp(-0.3, 0.0, enc);
O.pivote.rotation.set(baseX + O.sx.x * (1 - enc), baseY + O.sy.x * (1 - enc) + (quieto ? 0 : Math.sin(ESC.t * 0.15) * 0.05 * (1 - enc)), baseZ);
const x0 = movil() ? 0 : 0.95;
O.pivote.position.set(lerp(x0, 0, enc), lerp(movil() ? 0.95 : 0.42, 0.9, enc), lerp(0, 3.2, enc));
O.pivote.scale.setScalar((0.62 + 0.38 * k) * (movil() ? 0.62 : 0.84));
O.esc.environmentRotation.y = 0.9 + (quieto ? 0 : ESC.t * 0.09);
uComun.uDifr.value = 1.5 * k;
hudTilt.textContent = `${(O.sx.x * 57.3 >= 0 ? "+" : "")}${fmt(O.sx.x * 57.3, 1)}° / ${(O.sy.x * 57.3 >= 0 ? "+" : "")}${fmt(O.sy.x * 57.3, 1)}°`;
renderer.autoClear = true; renderer.render(O.esc, O.cam);
} else { renderer.clear(); }
ESC.etapa = "hero";
} else if (PR.zoomVisible && document.body.classList.contains("estatico")) {
// movimiento reducido: los cinco estados van en DOM; el lienzo se vacía
if (ESC.etapa !== "nada") { renderer.clear(); ESC.etapa = "nada"; }
} else if (PR.zoomVisible && ESC.zoom) {
const z = ESC.zoom; const q = PR.zoom;
const lg = logDeProgreso(q);
z.mat.uniforms.uLog.value = lg; z.mat.uniforms.uPulsos.value = 0;
z.px.o = P.nx; z.py.o = -P.ny; z.px.paso(dt); z.py.paso(dt); z.mat.uniforms.uPuntero.value.set(z.px.x, z.py.x);
z.mat.uniforms.uAmbar.value = actualizarZoomHUD(lg);
renderer.render(z.esc, z.cam); ESC.etapa = "zoom";
} else if (PR.infVisible && ESC.zoom) {
const z = ESC.zoom;
z.mat.uniforms.uLog.value = -0.8 - PR.inf * 0.25; z.mat.uniforms.uPulsos.value = quieto ? 0 : (INF.activo ? 1 : 0.35); z.mat.uniforms.uAmbar.value = 0;
z.px.o = P.nx * 0.5; z.py.o = -P.ny * 0.5; z.px.paso(dt); z.py.paso(dt); z.mat.uniforms.uPuntero.value.set(z.px.x, z.py.x);
document.body.classList.remove("ambar-on");
renderer.render(z.esc, z.cam); ESC.etapa = "inf";
} else if (PR.srvVisible && ESC.srv) {
const s = ESC.srv; const q = quieto ? 1 : PR.srv;
if (!s.esc.environment && O?.env) { s.esc.environment = O.env; s.esc.environmentIntensity = 0.9; }
// cámara: órbita de frente-alta a tres cuartos y acercamiento al zócalo A
const a = lerp(Math.PI + 0.75, Math.PI + 0.3, Math.min(1, q / 0.6)), el = lerp(0.9, 0.5, Math.min(1, q / 0.6)), r = lerp(9.4, 5.6, clamp((q - 0.55) / 0.45, 0, 1));
s.sx.o = P.nx * 0.12; s.sy.o = P.ny * 0.06; s.sx.paso(dt); s.sy.paso(dt);
const mira = tmp.set(lerp(0, -0.95, clamp((q - 0.55) / 0.45, 0, 1)), 0.05, lerp(-0.3, -0.4, clamp((q - 0.55) / 0.45, 0, 1)));
s.cam.position.set(mira.x + Math.sin(a + s.sx.x) * r * Math.cos(el + s.sy.x), mira.y + Math.sin(el + s.sy.x) * r, mira.z + Math.cos(a + s.sx.x) * r * Math.cos(el + s.sy.x));
s.cam.lookAt(mira);
// el S1 cae y encaja en el zócalo A (Blender: −0,95, 0,4, 0,068 → three: −0,95, 0,068, −0,4)
if (s.chip) {
const c = clamp(q / 0.38, 0, 1), ce = 1 - Math.pow(1 - c, 3);
const rebote = c >= 1 ? 0 : Math.sin(c * Math.PI) * 0.0;
s.chip.position.set(-0.95, lerp(2.4, 0.068, ce) + rebote, -0.4);
s.chip.rotation.set(lerp(-2.4, 0, ce), lerp(1.6, 0, ce), lerp(0.6, 0, ce));
if (s.sombra) { s.sombra.material.opacity = 0.15 + 0.7 * ce; s.sombra.scale.setScalar(1.8 - 0.8 * ce); }
}
// ventiladores: arrancan tras el encaje
const vel = clamp((q - 0.36) / 0.25, 0, 1);
for (const v of s.ventiladores) v.rotation.z += dt * vel * (quieto ? 0 : 42);
// LED en ola
if (s.leds) { const on = clamp((q - 0.42) / 0.3, 0, 1) * 24; const col = new THREE.Color(); for (let i = 0; i < 24; i++) { const k2 = clamp(on - i, 0, 1); col.set(COL.cian).multiplyScalar(0.05 + 2.2 * k2 * (0.75 + 0.25 * Math.sin(ESC.t * 6 + i))); s.leds.setColorAt(i, col); } s.leds.instanceColor.needsUpdate = true; }
// calor
const calor = clamp((q - 0.5) / 0.45, 0, 1);
if (s.calor) s.calor.material.uniforms.uCalor.value = calor;
srvTemp.textContent = `${fmt(41 + 26 * calor, 1)} °C`; srvRpm.textContent = `${fmt(Math.round(vel * 9800))} rpm`; srvW.textContent = `${fmt(0.3 + 2.1 * clamp((q - 0.36) / 0.4, 0, 1), 1)} kW`;
renderer.render(s.esc, s.cam); ESC.etapa = "srv";
} else if (PR.pieVisible && ESC.pie) {
const p = ESC.pie; p.px.o = quieto ? 0.2 : P.nx; p.py.o = quieto ? -0.1 : P.ny; p.px.paso(dt); p.py.paso(dt);
p.u.uPuntero.value.set(p.px.x, p.py.x); p.u.uScroll.value = (PR.pie - 1) * 0.5;
renderer.render(p.esc, p.cam); ESC.etapa = "pie";
} else {
if (ESC.etapa !== "nada") { renderer.clear(); ESC.etapa = "nada"; }
}
}
if (hayGL) {
renderer.setAnimationLoop((t) => fotograma(t));
document.addEventListener("visibilitychange", () => { if (document.hidden) renderer.setAnimationLoop(null); else { ultimoT = performance.now(); renderer.setAnimationLoop((t) => fotograma(t)); } });
} else {
const bucle = (t) => { fotograma(t); requestAnimationFrame(bucle); }; requestAnimationFrame(bucle);
}
// Sin WebGL: la barra de escala y los estados se muestran igualmente
if (!hayGL) { $("#zoom .fijo").style.display = "none"; }
```
## Guion visual del zoom (qué dibuja cada capa y por qué)
El fragment shader del zoom no usa geometría: trabaja en milímetros. Cada píxel calcula su posición `w = CENTRO + (p + uPuntero × 0,018) × W`, donde `p` es la coordenada de pantalla centrada (x en −0,5 … 0,5) y `W = 10^uLog` es el ancho de pantalla en mm. `CENTRO = (5,93, 6,22)` es el punto de la oblea hacia el que se cae: el centro del núcleo (2, 1) del chip que ocupa la celda (0, 0) de la retícula. Así todas las capas comparten el mismo sistema de coordenadas y el zoom es continuo: no hay cortes, solo fundidos por ventana logarítmica (`ventana(lg, hi, lo)` con plumas de ±0,35 décadas).
- **L0 · oblea.** Un disco de radio 150 mm con la muesca (círculo de 4,5 mm centrado en (0, −150)) y la retícula de paso 10,5 mm con chips de 9,7 mm. Solo se pintan los chips cuyo centro cabe en el radio útil (147 mm menos 7,4 mm de semidiagonal). Las bandas de arcoíris son un seno en `w` con `espectro()` (paleta coseno) y se desplazan con `uTiempo × 0,25`: recuerdan a la iridiscencia de la oblea 3D en el empalme. El bisel es una franja clara a 1,6 mm del borde.
- **L1 · chip.** Dentro de cada chip, la textura `chip-mapa-v1.webp` se lee con `uv = loc / 9,7 + 0,5`. El canal G codifica el tipo de bloque (0 calles, 1 núcleos, 2 caché, 3 PHY de HBM, 4 E/S, 5 SerDes, 6 anillo de sellado); `tinte()` le da color, R modula el brillo (altura) y B añade las pistas finas. Es la misma textura que da relieve a los chips de la oblea 3D (el mapa de normales se derivó de ella), por eso la transición oblea → chip parece la misma materia vista más cerca.
- **L2 · bloque de núcleos.** Celdas de 0,12 mm; el 65 % son matrices SRAM (rejilla de 8 × 8 bits con relieve), el resto lógica plana; buses cada dos celdas en Y y cada cuatro en X. El tinte viene del tipo de bloque de L1, así el zoom en un núcleo se ve azul y en la caché violeta.
- **L3 · enrutado.** Baldosas Manhattan de 4 µm elegidas por hash: recta horizontal, recta vertical, cruce o codo en L; pista de 0,44 µm (11 % de la celda) con arista clara arriba/derecha (`rim`) para dar volumen; vías (círculos de 0,4 µm) en el 45 % de las celdas; una capa M1 girada, cuatro veces más fina y tenue debajo. Los pulsos son segmentos de brillo que avanzan por las rectas: `fract(g.x × 0,045 − t × 0,85 + hash(fila) × 3)` para las horizontales y `fract(g.y × 0,045 − t × 0,7 + hash(columna) × 3)` para las verticales; `smoothstep(0,86, 1,0)` recorta el pulso y `halo` lo extiende media celda a cada lado (el «bloom» barato).
- **L4 · transistor.** Aletas verticales de 26 nm de paso y 7 nm de ancho con sombreado redondeado y arista; puertas horizontales de 48 nm de paso y 17 nm de ancho en el color de acento, con sombra proyectada sobre la aleta; contactos (círculos claros) entre puertas; el transistor situado en `CENTRO` se resalta con un halo cian que respira (`0,7 + 0,3·sin(3t)`).
- **Inundación ámbar.** `uAmbar` (0 … 1, calculado en JS entre uLog −0,8 y −3,3 con rampas de 0,4 décadas) mezcla el color con `uAmbarCol × (0,22 + 0,95 × luma) + 0,02`: una foto en blanco y negro vista bajo la luz amarilla de la sala de fotorresina. Al mismo tiempo `body.ambar-on` pone la interfaz en negro. Es el momento en que el acento inunda un bloque entero.
- **Acabado.** Viñeta (`smoothstep(1,25, 0,35, |p|)` corregida por el aspecto, entre 0,72 y 1) y grano por píxel de ±0,0175.
## Anatomía de los recursos (qué hay dentro de cada archivo)
- **`oblea-v1.glb`** está modelado en unidades de 100 mm: el disco mide 3,0 de diámetro, 0,024 de grosor (exagerado adrede: el canto se ve), bisel de 3 segmentos, muesca de 0,045 en −Y. Los 562 chips son cubos de 0,097 × 0,097 × 0,005 con chaflán de 0,0035 en las aristas superiores y UV 0–1 por chip (por eso el mapa de normales se repite en cada uno). Dos materiales: `Silicio` (canto, calles) y `Chip`. Está exportado con +Y arriba: en three.js el disco mira a +Y y por eso el código gira `g.scene.rotation.x = π/2` para encararlo a la cámara (+Z); tras ese giro la muesca queda abajo, como en el shader.
- **`sala-blanca-v1.hdr`** es un estudio modelado, no una foto: siete tiras de 9 × 0,18 a 3,2 de altura (fuerza 20–26, frías), un panel cenital tenue (0,05), una pared entera en ámbar (1,0, 0,62, 0,08 × 3,6) a −X, un cristal azul a +X, un punto cian al fondo y suelo técnico de placas oscuras. Al rotarlo (`environmentRotation.y = 0,9 + 0,09 t`) las siete tiras resbalan por la oblea y la pared ámbar aporta el reflejo cálido que el término `s3` del shader refuerza.
- **`chip-v1.glb`** mide 0,52 de lado (52 mm): sustrato verde oscuro de 0,026 de alto, tapa metálica de 0,034 con chaflán, die de 0,23 de lado con capa fina, 28 condensadores y una única bola BGA (`Bola`, esfera de 10 × 6) que la web instancia en 30 × 30 sin el hueco central de 8 × 8 (836 instancias).
- **`servidor-v1.glb`** es un chasis 1U de 4,4 × 7,0 × 0,44 destapado, con frontal de rejilla (celdas de 0,062 separadas 0,018, los huecos son los agujeros), placa de 3,9 × 4,8 centrada en y = 0,55 (Blender) → z = −0,55 (three.js), dos zócalos (A en x −0,95, B en x 0,95, ambos en y 0,4 → z −0,4), disipador de 26 aletas en B, 20 DIMM con disipador negro, 16 bobinas y 20 condensadores de VRM, PCH con 12 aletas, pistas de cobre, 6 ventiladores (marco + rotor de 7 palas, el rotor gira sobre su Z local) y fuente. El chip cae en (−0,95, 0,068, −0,4): 0,068 es la cota de la placa (0,06) más la base del zócalo (0,008). Los siete materiales del GLB (`Chasis`, `Placa`, `Zocalo`, `Aluminio`, `DIMM`, `Plastico`, `Cobre`) traen solo color plano: la web los sustituye por nombre con `PBR_SRV` (ver Escena C), pinta la placa con `texturaPlaca()` (canvas de 1024², semilla fija 260912: máscara `#0b2a1c`, planos de masa `#12382a`, pistas `#1e5a3c`, 700 pads `#c9a24a`, serigrafía blanca con los marcos de los zócalos en UV (0,256, 0,5) y (0,744, 0,5) y el rótulo «NODO S1-8 · REV C · NORVIK») y cepilla el aluminio con `texturaCepillado()` (vetas horizontales como `roughnessMap`, repetición 3 × 3). Todas las mallas tienen UV, por eso los mapas funcionan sin tocar el GLB.
- **Los renders** (`hero-poster`, `chip-macro`) salen de Cycles con la misma geometría, el mismo HDRI y capa fina en los chips, con AgX y fondo negro; sus mapas de profundidad son el pase Z convertido a disparidad (1/z) entre 4,2 y 8,5 unidades (hero) y entre 0,85 y 1,9 (chip macro), blanco = cerca. El pie usa el de chip macro con 6 iteraciones de punto fijo para que los bordes no se rompan al desplazar.
- **El loop** es la oblea vista de cerca con el entorno girando 360° en 120 fotogramas: el fotograma 120 es idéntico al 0, así que el bucle no tiene salto.
## Por qué cada número (para no «redondear» al reconstruir)
- 562 chips: los que caben en un disco de 300 mm con retícula de 10,5 mm y 3 mm de exclusión de borde, calculados uno a uno en el script que generó el GLB. El HUD dice «562 / 562 chips ok».
- 26 × 33 mm: el campo de exposición máximo de un escáner de litografía (la «retícula»); la intro lo cita y el título de métricas también.
- 1.200 TOPS INT8, 18,4 TOPS/W y 65 W: 1.200 / 65 = 18,46. El formulario usa 65 W por chip más 180 W de servidor por cada 8 chips: 8 chips → 700 W.
- 2,1 ms de latencia p99 y 21 ms de primer token con un modelo de 8.000 millones de parámetros: coherentes con 980 ± 160 tokens por segundo (≈ 1 ms por token en media, 2,1 en el percentil 99).
- 61,3 / 27,8 / 19,4 tokens por segundo y vatio: barras de 430 / 195 / 136 px sobre 440 de escala.
- Muelle 170 / 26: casi crítico (ζ ≈ 1,0 para k 170 y masa 1); la oblea llega sin oscilar pero con vida. El del zoom (60 / 14) y el del pie (50 / 12) son más lentos porque mueven encuadres, no objetos.
- Ventana del zoom: 7,45 décadas de log₁₀ (de 10³ a 10^−3,75 mm) en 520 svh, con el tramo «dentro de la pista» (uLog −1,5 → −2,95) comprimido en el 10 % del scroll para no aburrir.
- 2,4 s: tope de la intro; la retícula tarda 1,7 s y `setTimeout(saltar, 2400)` es la red de seguridad.
## Errores típicos al reconstruir esta pieza (y cómo se evitan)
1. **Oblea negra.** Con `metalness` alto, el F0 del metal es el color base: un color base oscuro no refleja nada y la iridiscencia no se ve. Por eso el silicio es `#9aa1ae` (metal 0,92) y los chips `#5a6172` (metal 0,72): el negro lo pone el entorno, no el material.
2. **`requestIdleCallback` que nunca llega.** Con el bucle a 60 fps el navegador puede no encontrar tiempo ocioso; sin `{ timeout: 1200 }` el servidor y el pie no se crean.
3. **`ScrambleText` con HTML.** El plugin trata el texto como texto: la unidad va en un `<small>` separado (`#zoom-uni`) y se descifra aparte.
4. **Visibilidad por `onToggle`.** Los `onToggle` de ScrollTrigger se pierden con saltos de scroll; la visibilidad se mide con `getBoundingClientRect()` en cada fotograma.
5. **Zona muerta entre capas.** Si el enrutado tiene celdas de 40 µm, entre 4 µm y 200 nm solo se ve cobre plano. Con celdas de 4 µm, una capa M1 cuatro veces más fina y el tramo rápido de `TRAMOS`, el ojo siempre tiene algo que leer.
6. **Empalme hero → zoom.** El disco del shader se dibuja relativo al ancho y la oblea 3D relativo al alto: `lgInicial()` iguala ambos con la escala (0,84 escritorio, 0,62 móvil), la distancia (8,1 − 3,2 = 4,9) y el fov (30°: alto visible 2,626).
7. **Separador de miles.** `toLocaleString("es-ES")` no agrupa los números de cuatro cifras: `fmt()` agrupa a mano con punto y coma decimal.
8. **Poster encima del canvas.** El `<picture>` del hero está por encima del lienzo (main tiene z-index 1): solo se muestra con `body.no-webgl` o `body.sin-glb`, y se apaga (`opacity: 0`) al cargar el GLB.
9. **Ventiladores que no giran.** El rotor se modeló con el eje en Y de Blender, que al exportar con +Y arriba pasa a −Z: se gira `rotation.z`, no `rotation.y`.
10. **Textos que entran dos veces.** `partir()` marca el `<span class="l">` con `data-ok` y no vuelve a partirlo; las entradas usan `overwrite: true`.
11. **Movimiento reducido a medias.** No basta con quitar la intro: el zoom pasa a cinco estados en DOM (`body.estatico`), el lienzo se limpia en esa sección, el servidor salta a q = 1 y el vídeo se sustituye por su póster.
12. **`#intro` oculto por CSS pero el JS esperando su fin.** Con `reducido` el JS elimina el nodo y pone los titulares en opacidad 1 sin animar; si no, el hero se quedaría vacío.
13. **Shader del zoom pintado en el hero.** Mientras `#hero` está en pantalla (`PR.heroVisible`) se dibuja SOLO la escena GLB de la oblea; el shader del zoom empieza cuando el hero ha salido del todo (`enc = 1`, `#zoom` visible). Si al 0 % se ve un disco gris con retícula, el orden del bucle está mal: a 0 % tienen que verse los 562 chips en relieve, lilas e iridiscentes.
14. **`uDifr` fijado solo al crear el uniform.** El bucle lo reescribe cada fotograma (`uComun.uDifr.value = 1.5 * k`); si se cambia el valor inicial y no el del bucle, la difracción no cambia.
15. **Anisotropía en el aluminio.** `MeshPhysicalMaterial.anisotropy` necesita tangentes; sin ellas (el GLB no las trae) las paredes del chasis salen con ruido de puntos. El cepillado se hace con un `roughnessMap` de vetas, no con anisotropía.
16. **Velo de la nav sobre el WebGL.** El velo (`body.nav-solida`) solo se activa cuando una sección DOM toca la franja de la nav; si se deja fijo, tapa la oblea, el zoom, el servidor y el pie. En `#norvik` (fondo hueso) la nav pasa a tinta con `body.claro-on`; sin eso, el menú desaparece en blanco sobre blanco.
## Interacción
- **Cursor = juego con la oblea.** El puntero normalizado (−1 … 1) es el objetivo de dos muelles (k 170, c 26); la oblea gira ±0,34 rad en X y ±0,42 rad en Y. Cuando gira, el vector reflejado cambia y las bandas de arcoíris del término de difracción recorren los chips; los reflejos de las tiras del HDRI resbalan. El anillo del cursor (34 px, cian, `mix-blend-mode: screen`, rótulo «inclinar») solo se ve en el hero y solo con puntero fino (`body.puntero-ok`).
- **Táctil**: en `#hero`, `touchmove` arrastra la oblea (200 px = rotación completa); `deviceorientation` la inclina con el móvil si el navegador lo emite sin permiso; si no hay nada en 2,6 s, vaivén automático. Nunca «esto solo va con ratón».
- **Scroll**: Lenis con `lerp 0.09`; rueda, arrastre táctil y teclado. El zoom y el servidor son secciones `position: sticky` cuyo progreso lo da un ScrollTrigger `top top → bottom bottom` con `scrub: true`.
- **Teclado**: cualquier tecla salta la intro; los enlaces del menú hacen `lenis.scrollTo` con easing `1 − (1−x)⁴` en 1,4 s; foco visible en cian (`outline 2px`).
- **Inferencia**: el formulario lanza `inferir()` con la pregunta; la respuesta se elige por palabras clave (reposo/consumo, latencia, precio; genérica si no). Los tokens aparecen cada 26–60 ms; el contador muestra 980 ± 160 tok/s con jitter. Se dispara sola la primera vez al llegar la sección al 55 %.
- **Formulario del kit**: valida empresa y email (`checkValidity`), el `range` recalcula al instante y el envío muestra «Solicitud recibida…» sin recargar.
- **Clic en el zoom**: no hay; el puntero desplaza el encuadre ±1,8 % con muelle (k 60, c 14) para que el circuito «respire».
- **Hover**: los enlaces de la nav se descifran (ScrambleText en mayúsculas, 0,55 s; su ancho se fija con `--w` al cargar las fuentes para que no salten); los botones `.btn` se rellenan de hueso de izquierda a derecha (`::before` con `scaleX`, 0,5 s) y el texto pasa a negro (a ámbar en el tramo ámbar); en las ofertas de empleo la flecha se rellena de tinta y avanza 4 px. Con movimiento reducido no hay transiciones.
- **Nav**: fija y siempre visible; solo cambia de color (negro con `body.ambar-on`, tinta con `body.claro-on` sobre Norvik) y recibe un velo con blur únicamente cuando una sección DOM pasa por debajo (`body.nav-solida`, medido cada fotograma en `navEstado()`).
## Adaptación a móvil y tablet
- Breakpoint único a 820 px (`@media (max-width:820px)`), responsive de 360 a 1920 px sin scroll horizontal.
- Hero: `min-height 640px`; `#t1` a 13 vh, `#t2` a 24 vh del fondo; se ocultan el HUD superior derecho y el inferior derecho; el párrafo baja a 13 px / 26 ch; la oblea se centra (x 0), sube (y 0,95) y se escala a 0,62; el menú central desaparece (queda logo y botón).
- Zoom: se oculta la lista de etapas; la cifra sube (bottom gutter + 110 px); la barra de escala mide 32 vw. El empalme hero → zoom usa `lgInicial()` con la escala móvil (0,62).
- Inferencia: una columna. Métricas: cifras a 2 × 2 y gráfico bajo el texto. Servidor: se oculta `.srv-p`; HUD a 2 columnas; título inferior a 8 vw. Norvik y kit: una columna; la columna de empleo esconde el rótulo de ubicación; el pie a 2 columnas.
- WebGL: `DPR ≤ 1,5`; el encapsulado no instancia las bolas; el mapa de sombras del foco baja a 1024; el HDRI ya es de 1024 × 512; los `<picture>` usan las variantes de 1280 px por debajo de 900 px.
- Zoom en móvil: `.zoom-etapas{display:none}`, `.barra-escala{width:32vw;bottom:calc(var(--gutter) + 70px)}` y la cifra a `bottom: gutter + 110px`, así la barra de escala y la frase no se solapan. El servidor mantiene fondo negro hasta el final de su sticky (la sección clara de Norvik empieza justo después).
## Accesibilidad y movimiento reducido
- HTML semántico: `header`, `main`, `section` con `aria-label`/`aria-labelledby`, `footer`, un solo `h1`, listas reales, `label` para cada campo (uno con clase `.sr`), `aria-live="polite"` en la salida de la inferencia, `aria-hidden` en todo lo decorativo (canvas, intro, cursor, pósteres, marquesina, mapa). El gráfico SVG lleva `role="img"` y un `aria-label` con los tres valores. Contraste AA: hueso sobre negro, tinta sobre hueso, negro sobre ámbar en el tramo ámbar.
- `prefers-reduced-motion: reduce` (`reducido`): la intro no se muestra; los titulares aparecen sin animación; la oblea queda en su mejor pose (objetivo fijo 0,25 / −0,15, entorno quieto, sin vaivén); el zoom se sustituye por los cinco estados finales en DOM (`body.estatico`: la sección deja de ser sticky y el lienzo se limpia); la inferencia escribe sin pausas y su fondo no tiene pulsos; las cifras y las barras saltan a su valor; el servidor se muestra en su estado final (q = 1, ventiladores parados); la marquesina no se mueve; el vídeo se sustituye por su póster.
- Sin WebGL (`body.no-webgl`): el canvas se elimina; el hero muestra el póster AVIF/WebP de la oblea; el zoom muestra los cinco estados; el servidor y el pie muestran el render del encapsulado; el resto es DOM y funciona igual.
## Rendimiento
- Un solo `WebGLRenderer` (`antialias`, `alpha`, `powerPreference: "high-performance"`), `ACESFilmicToneMapping` con exposición 1,05, `SRGBColorSpace`, DPR ≤ 2 (≤ 1,5 en móvil). Una escena por fotograma; el lienzo se limpia cuando ninguna sección WebGL está en pantalla; `setAnimationLoop(null)` con la pestaña oculta.
- Geometría: oblea 16.996 triángulos, servidor 5.172, encapsulado 568 + 836 instancias de una bola de 100 triángulos (solo escritorio). Llamadas de dibujo por debajo de 40 en la escena más cargada. Un solo mapa de sombras en toda la pieza: el foco del servidor (`SpotLight` 90, cono 0,55, penumbra 0,6, `castShadow`, 2048² en escritorio y 1024² en móvil, `PCFSoftShadowMap`, bias −0,0004, normalBias 0,02, cámara de sombra 2 → 16) sobre un suelo `ShadowMaterial` (opacidad 0,55) que ancla el chasis al negro; la luz no se mueve, solo el chip. La sensación de contacto la completan el disco de sombra bajo el chip, el mapa de calor y un `PointLight` cian en el frontal; el entorno PMREM del hero se reutiliza a intensidad 0,9 con una `HemisphereLight` (0x8fa3c4 / 0x0a0c10, 0,6). Sin DoF: la profundidad la da la viñeta `.srv-velo` (radial, transparente al 50 %, negro al 72 % en el borde). Las dos texturas de canvas (placa 1024², cepillado 512²) se generan una vez al crear la escena.
- Postproceso dentro de los shaders: grano (`hash21(gl_FragCoord.xy + fract(uTiempo)·17) − 0,5) × 0,035`), viñeta, halo de los pulsos cian (falso bloom por `smoothstep` ancho) y tonemapping ACES en las escenas 3D. Sin pases de composición ni DoF.
- Texturas: 1024² sin pérdida para normales y plano de planta (mipmaps, anisotropía 4 en el mapa), HDRI 1024 × 512 pasado por PMREM una sola vez y compartido por las dos escenas 3D.
- El vídeo de fondo se carga en diferido y se pausa fuera de pantalla; el pie sube el AVIF solo si el navegador lo decodifica (prueba con un AVIF de 1 px en data URI).
## Personalización
- Textos: `Silíceo`, `S1`, `Pensado`, `en silicio.` (la última palabra dentro de `<em>` para que vaya en acento), `Norvik`, `C/ Marie Curie, 14 · 48210 Parkside (Norvik)`, `+00 000 000 000`, `hola@siliceo.com`. Las letras de la marca en la intro se escriben una a una en `<span>`.
- Colores: `#050608`, `#ffb000` (ámbar: inundación del zoom, puertas del transistor, mapa de calor, botón lleno) y `#3be8ff` (cian: pulsos, LED, cursor, cifras de la consola). Viven en `:root` y los shaders los reciben como uniform (`uAmbarCol`, `uCian`) leídos con `getComputedStyle` al arrancar: no hay que tocar GLSL.
- Recursos: para usar modelos propios, exporta GLB con Draco y conserva los nombres de material (`Silicio`, `Chip`) y de nodo (`Zocalo_A`, `Ventilador_1…6`, `Bola`, `Die`); cambia solo la constante `ASSETS`.
- Cifras: los valores de métricas están en `data-n`; las etapas del zoom, en `ETAPAS`; las respuestas de la inferencia, en `RESP`; la fórmula del kit, en `calc()`.
## Comprobación final
1. Los 17 recursos cargan desde `https://motionframe.app/assets/signature-siliceo` sin errores de CORS ni 404 (pestaña Red: `oblea-v1.glb`, `sala-blanca-v1.hdr`, `chip-normal-v1.webp`, `chip-mapa-v1.webp`, `chip-v1.glb`, `servidor-v1.glb`, los AVIF/WebP y el loop).
2. Consola limpia: cero errores y cero peticiones fallidas al cargar y al recorrer la página entera.
3. La intro dura menos de 2,4 s, se salta con una tecla o un toque y, al acabar, los titulares entran por letras con desenfoque.
4. La oblea aparece iridiscente (bandas rosa, verde y oro que corren al mover el cursor) y se inclina con muelle; el HUD «Inclinación» cambia en grados y la hora UTC avanza cada segundo. A 0 % se ven los 562 chips en relieve, lilas e iridiscentes: es la escena GLB, nunca el shader del zoom (el disco gris con retícula solo aparece cuando `#zoom` entra en pantalla).
5. Al salir del hero, la oblea se encara, se centra y su tamaño coincide con el disco del shader del zoom (sin salto visible).
6. El zoom es continuo: la barra de escala pasa por mm, µm y nm; la cifra se descifra en cada etapa; en el tramo del enrutado la pantalla entera se inunda de ámbar y el menú pasa a negro; al final se ve el transistor con un punto cian. En el tramo ámbar la frase, el HUD de etapas y la barra de escala siguen legibles gracias al velo local `.zoom-velo`.
7. La inferencia escribe la respuesta token a token con el contador de tokens por segundo cerca de 1.000 y los pulsos cian recorren las pistas del fondo.
8. Las cuatro cifras cuentan hasta 1.200, 18,4, 2,1 y 96 y las barras del gráfico crecen; el loop de vídeo se reproduce detrás y se pausa fuera de pantalla.
9. En «Del silicio al centro de datos», el encapsulado cae y encaja en el zócalo A, los ventiladores giran, los LED se encienden en ola y el mapa de calor se extiende; la cámara acaba cerca del zócalo con 67,0 °C, 9.800 rpm y 2,4 kW. Se ven aluminio cepillado, la placa verde con serigrafía y pads, las sombras que el foco proyecta sobre la placa y la sombra de contacto que se concentra bajo el chip.
10. El mapa de Norvik se traza a línea y el formulario del kit recalcula TOPS, potencia y ocupación al mover el `range`; el envío valida y muestra la confirmación.
11. El pie muestra el render del encapsulado con parallax que sigue al puntero y al scroll; la marquesina gira sin cortes.
12. 60 fps en escritorio con gráfica integrada y ≥ 40 fps en un Android de gama media (CPU ×4 más lenta en DevTools), sin scroll horizontal de 360 a 1920 px.
13. Con `prefers-reduced-motion: reduce` no hay intro ni muelles; el zoom se ve como cinco estados finales y el servidor en su estado final: una composición estática terminada.
14. Con WebGL desactivado aparecen los pósteres AVIF/WebP y el resto de la página funciona en DOM.
15. Teclado: se puede recorrer el menú, el formulario de inferencia y el del kit con Tab, con foco visible en cian; los enlaces `#` desplazan suavemente. La nav no desaparece en ninguna sección: velo con blur sobre `#inferencia`, `#metricas`, `#norvik` y `#kit`, tinta sobre Norvik, sin velo sobre las escenas WebGL. Hover: los enlaces de la nav se descifran, los botones se rellenan de izquierda a derecha y las flechas de empleo se rellenan de tinta.Prueba en frío
Pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos el resultado con la demo. Prueba hecha el 30-09-2026.

Calca la demo captura a captura en escritorio y móvil: oblea lila iridiscente, zoom de cobre con pulsos cian, servidor 3D con calor contenido y pie con marquesina. Solo cambian valores vivos (hora, contadores, inclinación).

Mejora clara: la oblea ya tiene chips iridiscentes, el enrutado es cobre y el titular de métricas recupera el ámbar. Pero la oblea sale casi plana y más gris que lila, el cobre es un tejido denso en vez del laberinto de pistas, el calor del servidor quema toda la placa y en móvil el párrafo del hero se monta sobre la oblea.
Solo publicamos lo que hemos probado. Otras herramientas (ChatGPT, Gemini, Lovable…) pueden dar resultados distintos.
Paso a paso
Personaliza
Copia el Completo o el Lite
Pega e itera
Publica
Licencia en claro
Todas las piezas incluyen licencia de uso comercial. Usa el resultado tal cual en tus productos, encargos freelance o webs de clientes, sin atribución.
Puedes
No puedes
Es la misma licencia para lo gratis y lo Pro. La tienes entera en licencia de uso.
Preguntas frecuentes
Versión 1.0 · publicada el 30-09-2026

Comercio · Estudios creativos

Comercio · Estudios creativos

Comercio · Tecnología

Hostelería · Eventos

Inmobiliaria · Arquitectura

Deporte · Salud y estética

Arquitectura · Inmobiliaria

Salud y estética · Comercio