Clarix
aadmin2026-11-05 发布Site模板8 阅读

Clarix

类型:Site模板 · 收录:2026-11-05 01:13:02 · 源码:会员可见

[hidecontent type="vip1"]
# Recreate this site as a single HTML file: Clarix

You are an expert creative front-end developer. Produce a **single self-contained `index.html`**
that reproduces the project below **exactly** — same layout, sections, visuals, motion, and
interaction. Pure HTML/CSS/JS in one file: no build step, no framework, no bundler. Use ES modules
with a CDN importmap for the libraries actually used (Three.js r0.184 and Lenis r1.3 from unpkg).
Hardcode every value given here as a fixed constant. All the CSS below lives in one `<style>` block
in `<head>`; the script in one `<script type="module">` block before `</body>`.

## What it is

A single-page, white-background AI-agency landing site ("IrisAI") built around **one continuous,
cinematic scroll** that is `2800vh` tall. The whole experience is choreographed against a single
normalized scroll progress `0 → 1`. Two stacked full-screen WebGL canvases drive the visuals: a
**fixed background canvas** running a procedural animated "ASCII grid" shader (rounded-box cells
that bloom in a rainbow gradient and react to the mouse), and a **fixed foreground canvas** holding
a dark 3D model wrapped in a holographic procedural shader that itself is dithered into an ASCII
half-tone, plus a particle system that assembles the IrisAI logo out of thousands of points.

The flow: an **odometer preloader** counts `000 → 100` in stepped jumps while a wobbling blurred
blob pulses on the model's spot; at 100 the blob shatters outward and the hero reveals — the title
flips up letter-by-letter (3D rotateX + blur), and the model is carved into view by an expanding,
glowing screen-space reveal circle. Then as you scroll: the model drops and spins into frame; giant
HTML "flying words" (Tools / kill / growth) sweep across in 3D perspective; a frosted-glass stats
panel (gradient image + 200+ / 97% / 10X) slides up and away; a split feature block fades in; the
camera flies *into* the scene as the logo particles spin-assemble; the assembled logo holds, then
shatters as the model flies back to center for a finale title ("The future of digital art"); and a
glassmorphism footer slides up. Palette: white `#ffffff` / near-white `#f6fafe` page, black text, and
a five-stop rainbow accent — blue `#0091ff`, cyan `#00aaff`, purple `#8000ff`, hot-pink `#ff0099`,
red `#ff0000`. Font: Google "Sarabun" (weights 200/400, italic 400).

## Page shell & libraries

In `<head>`:

```html
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Model</title>
<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=Sarabun:ital,wght@0,200;0,400;1,400&display=swap" rel="stylesheet">
```

Importmap + module entry before `</body>`:

```html
<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.184.0/build/three.module.js",
    "three/examples/jsm/": "https://unpkg.com/three@0.184.0/examples/jsm/",
    "lenis": "https://unpkg.com/lenis@1.3.23/dist/lenis.mjs"
  }
}
</script>
<script type="module"> /* all the JS below */ </script>
```

Module imports actually used:

```js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import Lenis from 'lenis';
```

(The original also imports SVGLoader and MeshSurfaceSampler but they are unused — omit them. There
is no control-panel library; omit it entirely.)

### Lenis smooth scroll

```js
const lenis = new Lenis({
  duration: 1.5,
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t))
});
```

Lenis is driven from inside the rAF loop with `lenis.raf(time)` (see Animation loop). Native scroll
is what the `scroll` listener reads, so do NOT pass `smoothWheel:false`; let Lenis run normally.

### Global resets & base CSS (verbatim — put in the head `<style>`)

```css
/* Hide scrollbar for WebKit browsers */
::-webkit-scrollbar { display: none; }
html {
  /* Base 16px at 1920px width. Clamps at 12px for smaller screens to maintain readability */
  font-size: max(12px, 0.833333vw);
}
body {
  margin: 0;
  overflow-x: hidden;
  -ms-overflow-style: none;
  scrollbar-width: none;
  background-color: #ffffff;
  height: 2800vh; /* Long cinematic scroll */
}
.preload-hidden { opacity: 0 !important; pointer-events: none !important; }
.reveal-anim {
  transition: transform 1.6s cubic-bezier(0.2, 0.8, 0.2, 1) 0.6s, opacity 1.6s ease 0.6s;
  will-change: transform, opacity;
}
.preload-hidden.reveal-anim.from-top { transform: translateY(-20px); }
.preload-hidden.reveal-anim.from-bottom { transform: translateY(20px); }
canvas { display: block; }
```

The two WebGL canvases are created in JS and appended to `<body>` as `position:fixed; top:0; left:0`
elements. Background canvas `z-index:1`, foreground canvas `z-index:3`. The hero title sits at
`z-index:2` (between them) so it reads as embedded in the scene.

## Layout & sections (in order)

All text uses `font-family: 'Sarabun', sans-serif` unless noted. Copy is verbatim — reproduce
exactly including punctuation and `<br>` placement.

### CSS for all DOM overlays (verbatim)

```css
.hero-logo { position: absolute; top: 2.5rem; left: 3.125rem; z-index: 5; height: 1.75rem; width: auto; }
.hero-title {
  position: absolute; top: 43%; transform: translateY(-50%); left: 3.125rem;
  font-family: 'Sarabun', sans-serif; font-weight: 200;
  font-size: max(2.5rem, 7vw); color: #000000; margin: 0; z-index: 2;
  pointer-events: none; line-height: 1.0; white-space: nowrap;
}
.char-mask {
  display: inline-block; overflow: hidden; vertical-align: bottom;
  padding-top: 0.2em; margin-top: -0.2em;
  padding-bottom: 1.2em; margin-bottom: -1.2em; perspective: 500px;
}
.hero-char {
  display: inline-block;
  transform: translateY(110%) translateZ(-60px) rotateX(-65deg);
  transform-origin: bottom center; filter: blur(14px); opacity: 0;
  will-change: transform, filter, opacity;
  transition: transform 1.6s cubic-bezier(0.2, 0.8, 0.2, 1), filter 1.6s ease, opacity 1.2s ease;
}
.hero-char.revealed { transform: translateY(0) translateZ(0) rotateX(0deg); filter: blur(0px); opacity: 1; }
/* Non-hero titles animate much faster for snappy scrolling */
.animated-title .hero-char {
  transition: transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), filter 0.8s ease, opacity 0.6s ease;
}
.hero-nav {
  position: absolute; top: 2.5rem; right: 3.125rem; z-index: 5;
  font-family: 'Sarabun', sans-serif; font-weight: 400;
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.75rem;
}
.hero-nav a {
  color: #000000; text-decoration: none; font-size: 1.1rem; font-weight: 400;
  text-transform: uppercase; display: flex; align-items: center; gap: 0.5rem;
  opacity: 1; transition: transform 0.2s ease; cursor: pointer;
}
.hero-nav a:hover { transform: translateX(-4px); }
.hero-nav a .nav-arrow { font-size: 1.2rem; font-weight: 400; line-height: 1; }
.hero-subtitle { font-weight: 400; color: #000000; }
.hero-info-block {
  position: absolute; bottom: 3.125rem; left: 3.125rem; z-index: 5;
  font-family: 'Sarabun', sans-serif; color: #000000; width: 20rem;
  display: flex; flex-direction: column;
}
.hi-top { display: flex; justify-content: space-between; font-size: 1.1rem; font-weight: 400; text-transform: uppercase; margin-bottom: 2rem; }
.hi-text { font-size: 1.0625rem; line-height: 1.5; opacity: 0.7; margin-bottom: 2.5rem; }
.hi-btn {
  align-self: flex-start; padding: 0.875rem 3rem; border: 1px solid #000000;
  border-radius: 2.5rem; background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: #000; text-decoration: none; font-size: 0.95rem; font-weight: 400;
  letter-spacing: 1px; transition: background 0.2s, transform 0.2s;
}
.hi-btn:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); }
.scroll-content-wrapper {
  position: absolute; top: calc(100vh + 34.375rem); left: 0; width: 100%;
  z-index: 2; display: flex; flex-direction: column;
}
.scroll-title-container { padding-left: 3.125rem; margin-bottom: 37.5rem; }
.scroll-title-container h3 {
  font-family: 'Sarabun', sans-serif; font-weight: 200; font-size: max(2.625rem, 5.5vw);
  letter-spacing: -0.02em; white-space: nowrap; color: #000; margin: 0; line-height: 1.1;
}
.clients-container { width: 100%; display: flex; flex-direction: column; margin-bottom: 37.5rem; }
.clients-subtitle { font-family: 'Sarabun', sans-serif; font-size: 1.1rem; font-weight: 400; text-transform: uppercase; padding-left: 3.125rem; margin-bottom: 1rem; color: #000; }
.clients-line {
  height: 1px; margin-left: 3.125rem; margin-right: 3.125rem;
  background: linear-gradient(to right, #0091ff, #00aaff, #8000ff, #ff0099, #ff0000);
  margin-bottom: 3rem;
}
.marquee-container { width: 100%; overflow: hidden; white-space: nowrap; filter: grayscale(100%); opacity: 0.8; }
.marquee-content { display: inline-flex; align-items: center; gap: 6rem; padding-right: 6rem; animation: marquee 25s linear infinite; }
@keyframes marquee { 0% { transform: translateX(-50%); } 100% { transform: translateX(0); } }
.marquee-logo { height: 3.75rem; width: auto; object-fit: contain; }
.feature-container { width: 100%; display: flex; flex-direction: column; }
.feature-title {
  font-family: 'Sarabun', sans-serif; font-weight: 200; font-size: max(2.625rem, 5.5vw);
  letter-spacing: -0.02em; white-space: nowrap; color: #000; margin: 0; line-height: 1.1;
}
.split-feature-container {
  width: 100%; display: flex; flex-direction: column;
  padding-left: 3.125rem; padding-right: 3.125rem; box-sizing: border-box; margin-top: 37.5rem;
}
.pill-badge {
  display: inline-block; font-family: 'Sarabun', sans-serif; font-size: max(0.875rem, 1vw);
  font-weight: 400; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 50px;
  padding: 0.3rem 1.2rem; color: #000;
}
.split-bottom { display: flex; flex-direction: column; align-items: flex-start; max-width: 45rem; }
.feature-text { font-family: 'Sarabun', sans-serif; font-size: max(1.1rem, 1.25vw); line-height: 1.5; font-weight: 300; color: #000; opacity: 0.8; }
.phase4-container {
  position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 5;
  pointer-events: none; display: flex; align-items: flex-end; justify-content: flex-end;
  opacity: 0; transform: translateY(50px);
}
.phase4-container .split-feature-container {
  pointer-events: auto; margin-top: 0; padding-top: 4rem; padding-bottom: 4rem;
  height: 100vh; justify-content: space-between; width: auto; align-items: flex-start;
}
.phase5-container { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 4; pointer-events: none; opacity: 0; }
.phase5-title-left { position: absolute; top: 2.5rem; left: 3.125rem; text-align: left; margin: 0; }
.phase5-title-right { position: absolute; bottom: 2.5rem; right: 3.125rem; text-align: right; margin: 0; }
.phase5-desc-left { position: absolute; bottom: 2.5rem; left: 3.125rem; text-align: left; max-width: 25rem; margin: 0; }
.phase5-label-right { position: absolute; top: 2.5rem; right: 3.125rem; width: 12rem; margin: 0; }
.phase6-container { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 2; pointer-events: none; opacity: 0; display: flex; align-items: center; justify-content: center; }
.phase6-title {
  position: absolute; top: 30vh; transform: translateY(-50%); left: 0; width: 100%;
  font-family: 'Sarabun', sans-serif; font-weight: 200; font-size: max(2.5rem, 7vw);
  color: #000; margin: 0; line-height: 1.0; text-align: center; white-space: nowrap; pointer-events: none;
}
.phase6-footer-wrapper { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 4; pointer-events: none; transform: translateY(100vh); }
.phase6-footer-wrapper * { pointer-events: auto; }
.glass-footer {
  position: absolute; bottom: 2.5rem; left: 3.125rem; right: 3.125rem; padding: 3rem;
  background: rgba(255, 255, 255, 0.4); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border-radius: 2rem; border: 1px solid rgba(255, 255, 255, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
  display: flex; flex-direction: column; color: #000; font-family: 'Sarabun', sans-serif;
}
.footer-top { display: flex; justify-content: space-between; }
.footer-left { width: 35%; display: flex; flex-direction: column; }
.footer-heading { font-size: 1rem; font-weight: 500; margin-bottom: 1.5rem; }
.footer-input-group { display: flex; background: rgba(255, 255, 255, 0.6); border-radius: 2rem; padding: 0.3rem; margin-bottom: 1rem; border: 1px solid rgba(0, 0, 0, 0.1); max-width: 22rem; }
.footer-input { flex: 1; background: transparent; border: none; outline: none; padding-left: 1rem; font-family: 'Sarabun', sans-serif; font-size: 0.9rem; }
.footer-submit { background: #000; color: #fff; border: none; border-radius: 2rem; padding: 0.6rem 1.5rem; font-family: 'Sarabun', sans-serif; cursor: pointer; transition: transform 0.2s; }
.footer-submit:hover { transform: scale(1.05); }
.footer-subtext { font-size: 0.75rem; opacity: 0.6; line-height: 1.4; max-width: 22rem; }
.footer-right { display: flex; gap: 5rem; padding-right: 2rem; }
.footer-col { display: flex; flex-direction: column; gap: 1rem; }
.footer-col-title { font-size: 0.9rem; font-weight: 500; margin-bottom: 0.5rem; }
.footer-col a { color: #000; opacity: 0.7; text-decoration: none; font-size: 0.85rem; transition: opacity 0.2s; }
.footer-col a:hover { opacity: 1; }
.footer-line { height: 1px; background: rgba(0, 0, 0, 0.1); margin-top: 3rem; margin-bottom: 1.5rem; }
.footer-bottom { text-align: center; font-size: 0.8rem; opacity: 0.6; }
.words-container { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; perspective: 1500px; z-index: 2; pointer-events: none; }
.flying-word {
  position: absolute; top: 50%; left: 50%; font-family: 'Sarabun', sans-serif; font-weight: 400;
  font-size: max(80px, 25vw); color: #000000; margin: 0; padding: 0 0.2em; opacity: 0;
  white-space: nowrap; transform: translate3d(-50%, -50%, 0) rotateY(0deg);
}
.flying-word .char { display: inline-block; padding: 0 0.15em; margin: 0 -0.15em; }
.glass-container {
  position: fixed; bottom: 0; left: 0; width: 100%; height: 100vh;
  background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(30px);
  border-top: 1px solid rgba(255, 255, 255, 0.4); z-index: 10; transform: translateY(100vh); pointer-events: none;
}
.glass-gradient-wrapper {
  position: absolute; top: 50%; left: 50%; width: calc(100% - 8rem); height: calc(100% - 8rem);
  transform: translate(-50%, -50%) scale(0.65); border-radius: 0px; overflow: hidden; pointer-events: auto;
}
.glass-gradient-img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; opacity: 0.5; }
.gradient-content {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; padding: 2rem;
  box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between;
  color: #000000; font-family: 'Sarabun', sans-serif;
}
.gc-top-left { font-size: max(2.625rem, 5.5vw); font-weight: 200; line-height: 1.1; letter-spacing: -0.02em; color: #000000; max-width: none; white-space: nowrap; }
.gc-bottom { display: flex; justify-content: space-between; align-items: flex-end; }
.gc-bottom-left { font-size: 14px; font-weight: 400; opacity: 0.8; }
.gc-bottom-right { display: flex; gap: 3.75rem; align-items: flex-end; }
.stat-col { position: relative; padding-left: 20px; display: flex; flex-direction: column; gap: 16px; }
.stat-col-tall { margin-bottom: 120px; }
.stat-col::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 1px;
  background: linear-gradient(to bottom, rgba(0,0,0,0.0), rgba(0,0,0,0.2) 10%, rgba(0,0,0,0.2) 90%, rgba(0,0,0,0.0));
}
.stat-col-tall::before { bottom: -120px; }
.stat-num { font-size: max(42px, 5.5vw); font-weight: 200; line-height: 1; }
.stat-desc { font-size: 13px; line-height: 1.5; opacity: 0.85; }
.stat-sub { opacity: 0.5; font-size: 11px; }
.stat-btn {
  width: 44px; height: 44px; border-radius: 50%; background: rgba(0, 0, 0, 0.05);
  display: flex; align-items: center; justify-content: center; margin-top: 8px; cursor: pointer;
  transition: background 0.2s; border: 1px solid rgba(0,0,0,0.1);
}
.stat-btn:hover { background: rgba(0, 0, 0, 0.15); }
```

### DOM body (verbatim — route images through ASSET_BASE_URL, see Assets)

```html
<!-- PRELOADER ODOMETER -->
<div id="preloader" style="position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 9999; pointer-events: none; transition: opacity 1s ease;">
  <div style="position: absolute; top: 2.5rem; left: 3.125rem; color: #000; font-family: 'Sarabun', sans-serif; font-weight: 200; font-size: 4rem; letter-spacing: -0.02em; line-height: 1;">
    [  
    <div style="height: 4rem; overflow: hidden; position: relative; width: 6.5rem; display: inline-block; vertical-align: top;">
      <div id="odometer-numbers" style="position: absolute; top: 0; left: 0; width: 100%; transition: transform 0.05s linear; text-align: center;"></div>
    </div>
      / 100 ]
  </div>
</div>

<img src="ASSET(logo.svg)" class="hero-logo preload-hidden reveal-anim from-top" alt="IrisAI Logo" />
<h1 class="hero-title" id="animated-hero-title">The clarity your business<br><span class="hero-subtitle">has been missing</span></h1>

<div class="hero-nav preload-hidden reveal-anim from-top">
  <a href="#">home <span class="nav-arrow">↗</span></a>
  <a href="#">plans <span class="nav-arrow">↗</span></a>
  <a href="#">services <span class="nav-arrow">↗</span></a>
  <a href="#">contact us <span class="nav-arrow">↗</span></a>
</div>

<div class="hero-info-block preload-hidden reveal-anim from-bottom">
  <div class="hi-top"><span>( A )</span><span>[ 001 /004 ]</span></div>
  <div class="hi-text">IrisAI is where your boldest ideas meet their full potential. We give you the tools not just to see the future.</div>
  <a href="#" class="hi-btn">GET STARTED</a>
</div>

<div class="scroll-content-wrapper">
  <div class="scroll-title-container" id="scroll-title">
    <h3 class="animated-title flow-title">Empowering visionaries<br>to shape tomorrow's<br><span style="font-weight: 500;">digital landscapes</span></h3>
  </div>

  <div class="clients-container">
    <div class="clients-subtitle">OUR CLIENTS</div>
    <div class="clients-line"></div>
    <div class="marquee-container">
      <div class="marquee-content">
        <!-- Two identical sets of 11 logos so the -50%→0 loop is seamless -->
        <!-- Set 1 and Set 2, each: Discord, Facebook, Snapchat, Spotify, TikTok, WhatsApp, YouTube, dribbble, ebay, instagram, twitter -->
        <img class="marquee-logo" src="ASSET(companies/Discord.png)" alt="Discord"> ... (repeat all 11, twice)
      </div>
    </div>
  </div>

  <div class="feature-container" style="align-items: flex-end; padding-right: 3.125rem; padding-left: 3.125rem; box-sizing: border-box;">
    <h3 class="feature-title animated-title flow-title" style="text-align: right;">Redefining digital experiences<br>through art and<br><span style="font-weight: 500;">technology</span></h3>
  </div>
</div>

<div class="phase4-container" id="phase4-container" style="font-family: 'Sarabun', sans-serif;">
  <div class="split-feature-container">
    <div class="hi-top" style="width: 100%; min-width: 15rem; max-width: 20rem; margin-bottom: 0;">
      <span>( A )</span><span>[ 001 /004 ]</span>
    </div>
    <div style="display: flex; flex-direction: column; align-items: flex-start;">
      <div class="split-top" style="text-align: left; display: inline-block;">
        <h3 class="feature-title animated-title" id="phase4-title" style="text-align: left; margin-top: 0;">Iris unlocks a faster,<br><span style="font-weight: 500;">smarter way to create</span></h3>
        <div class="clients-line" style="margin-top: 3rem; margin-bottom: 3rem; width: 100%; margin-left: 0; margin-right: 0;"></div>
      </div>
      <div class="split-bottom" style="align-items: flex-start;">
        <div class="hi-text" style="text-align: left; max-width: 30rem;">With just a prompt, you can bring any idea to life instantly, cut down production time, eliminate repetitive tasks, and stay fully in control of your creative vision.</div>
        <a href="#" class="hi-btn" style="margin-top: 1.5rem; text-transform: uppercase;">LAUNCH NOW</a>
      </div>
    </div>
  </div>
</div>

<div class="words-container">
  <div class="flying-word" id="word1"><span class="char">T</span><span class="char">o</span><span class="char">o</span><span class="char">l</span><span class="char">s</span></div>
  <div class="flying-word" id="word2"><span class="char">k</span><span class="char">i</span><span class="char">l</span><span class="char">l</span></div>
  <div class="flying-word" id="word3"><span class="char">g</span><span class="char">r</span><span class="char">o</span><span class="char">w</span><span class="char">t</span><span class="char">h</span></div>
</div>

<div class="glass-container" id="glass-container">
  <div id="glass-gradient" class="glass-gradient-wrapper">
    <img src="ASSET(gradient.png)" class="glass-gradient-img" alt="Gradient" />
    <div class="gradient-content">
      <div class="gc-top-left animated-title" id="phase3-title">Every project is a fusion of strategy,<br>creativity and passion</div>
      <div class="gc-bottom">
        <div class="gc-bottom-left">Innovation philosophy</div>
        <div class="gc-bottom-right">
          <div class="stat-col stat-col-tall">
            <div class="stat-num">200+</div>
            <div class="stat-desc">universes created<br><span class="stat-sub">*or maybe more</span></div>
            <div class="stat-btn">→</div>
          </div>
          <div class="stat-col">
            <div class="stat-num">97%</div>
            <div class="stat-desc">clients trust us for their<br>next project</div>
            <div class="stat-btn">→</div>
          </div>
          <div class="stat-col">
            <div class="stat-num">10X</div>
            <div class="stat-desc">team turnover<br><span class="stat-sub">*or maybe more</span></div>
            <div class="stat-btn">→</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<div class="phase5-container" id="phase5-container" style="font-family: 'Sarabun', sans-serif;">
  <div class="hi-top phase5-label-right"><span>( A )</span><span>[ 001 /004 ]</span></div>
  <h3 class="feature-title phase5-title-left animated-title" id="phase5-title-1">The pinnacle of generative art</h3>
  <div class="hi-text phase5-desc-left">Experience a new paradigm where your imagination meets our computational engine. Build entire worlds without writing a single line of code.</div>
  <h3 class="feature-title phase5-title-right animated-title" id="phase5-title-2">Designed for<br><span style="font-weight: 500;">visionaries</span></h3>
</div>

<div class="phase6-container" id="phase6-container">
  <h3 class="phase6-title animated-title" id="phase6-title">The future of<br><span style="font-weight: 400;">digital art</span></h3>
</div>

<div class="phase6-footer-wrapper" id="phase6-footer">
  <div class="glass-footer">
    <div class="footer-top">
      <div class="footer-left">
        <h2 style="margin: 0 0 2rem 0; font-weight: 500; font-size: 1.5rem; letter-spacing: -0.02em;">IrisAI</h2>
        <div class="footer-heading">Sign up to receive updates.</div>
        <div class="footer-input-group">
          <input type="email" placeholder="Enter your email" class="footer-input">
          <button class="footer-submit">Submit</button>
        </div>
        <div class="footer-subtext">By subscribing you agree to our Privacy Policy and provide consent to receive updates from our company.</div>
      </div>
      <div class="footer-right">
        <div class="footer-col"><div class="footer-col-title">Products</div><a href="#">Studio</a><a href="#">Engine</a><a href="#">Enterprise</a></div>
        <div class="footer-col"><div class="footer-col-title">Learn</div><a href="#">Blog</a><a href="#">Research</a><a href="#">Documentation</a></div>
        <div class="footer-col"><div class="footer-col-title">About</div><a href="#">Company</a><a href="#">Careers</a><a href="#">Contact</a></div>
        <div class="footer-col"><div class="footer-col-title">Legal</div><a href="#">Terms & Conditions</a><a href="#">Privacy Policy</a></div>
      </div>
    </div>
    <div class="footer-line"></div>
    <div class="footer-bottom">© 2026 IrisAI, All rights reserved.</div>
  </div>
</div>
```

## The WebGL scene

### Two scenes, two renderers, camera, lights, controls (verbatim)

```js
const bgScene = new THREE.Scene();
bgScene.background = new THREE.Color(0xf6fafe);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1, 5);

const rendererBg = new THREE.WebGLRenderer({ antialias: true });
rendererBg.setSize(window.innerWidth, window.innerHeight);
rendererBg.setPixelRatio(window.devicePixelRatio);
rendererBg.domElement.style.position = 'fixed';
rendererBg.domElement.style.top = '0';
rendererBg.domElement.style.left = '0';
rendererBg.domElement.style.zIndex = '1';
document.body.appendChild(rendererBg.domElement);

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.domElement.style.position = 'fixed';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '3';
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enabled = false;
controls.enableDamping = true;
controls.dampingFactor = 0.05;

const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.0);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight2.position.set(-5, -5, -5);
scene.add(directionalLight2);
```

### Shared uniforms (verbatim)

```js
let mixer;
const clock = new THREE.Clock();
const targetMouse = new THREE.Vector2(0.5, 0.5);
const _revealVec = new THREE.Vector3();
const shaderUniforms = {
  time: { value: 0 },
  mouse: { value: new THREE.Vector2(0.5, 0.5) },
  resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
  bgColor: { value: new THREE.Color(0xf6fafe) },
  colorCyan: { value: new THREE.Color(0x00aaff) },
  colorPurple: { value: new THREE.Color(0x8000ff) },
  colorBlue: { value: new THREE.Color(0x0091ff) },
  colorPeach: { value: new THREE.Color(0xff0000) },
  colorHotPink: { value: new THREE.Color(0xff0099) },
  edgeBlurAmount: { value: 0.5 },
  uRevealProgress: { value: 0.0 },
  uRevealOrigin: { value: new THREE.Vector2(0.5, 0.5) }
};
```

(There is no GUI/control panel — drop it entirely; the constants it once edited are baked above and
in the model placement below: `phase4X = -1.35`, `phase4Y = -5.4`, `phase4Z = 5.67`,
`phase4Scale = 0.85`, `phase4RotX = 0`, `phase4RotY = 0.6723`, `phase4RotZ = 0`. Store these in a
plain object `const guiParams = { phase4X:-1.35, phase4Y:-5.4, phase4Z:5.67, phase4Scale:0.85,
phase4RotX:0, phase4RotY:0.6723, phase4RotZ:0 };`.)

### A. Background ASCII grid (full-screen shader plane in `bgScene`)

A `PlaneGeometry(2,2)` mesh whose vertex shader writes clip-space directly so it ignores the camera;
rendered by `rendererBg`. `depthWrite:false, depthTest:false`, `renderOrder = -1`,
`frustumCulled = false`.

```js
const bgGeo = new THREE.PlaneGeometry(2, 2);
const bgMat = new THREE.ShaderMaterial({
  uniforms: {
    time: shaderUniforms.time, mouse: shaderUniforms.mouse, bgColor: shaderUniforms.bgColor,
    resolution: shaderUniforms.resolution, colorCyan: shaderUniforms.colorCyan,
    colorBlue: shaderUniforms.colorBlue, colorHotPink: shaderUniforms.colorHotPink, colorPeach: shaderUniforms.colorPeach
  },
  vertexShader: `...`, fragmentShader: `...`, depthWrite: false, depthTest: false
});
const bgMesh = new THREE.Mesh(bgGeo, bgMat);
bgMesh.renderOrder = -1; bgMesh.frustumCulled = false;
bgScene.add(bgMesh);
```

Vertex shader (verbatim):

```glsl
varying vec2 vUv;
void main() {
  vUv = uv;
  // Lock the plane to the screen completely ignoring the camera
  gl_Position = vec4(position.xy, 1.0, 1.0);
}
```

Fragment shader (verbatim):

```glsl
uniform float time;
uniform vec2 mouse;
uniform vec3 bgColor;
uniform vec2 resolution;
uniform vec3 colorCyan;
uniform vec3 colorBlue;
uniform vec3 colorHotPink;
uniform vec3 colorPeach;
varying vec2 vUv;

// 2D Rotation for organic noise
mat2 rotate2d(float _angle){
    return mat2(cos(_angle),-sin(_angle),
                sin(_angle),cos(_angle));
}

void main() {
   float pixelSize = 15.0;
   vec2 screenUv = vUv;
   float aspect = resolution.x / resolution.y;
   vec2 aspectUv = vec2(screenUv.x * aspect, screenUv.y);

   vec2 gridUv = aspectUv * (resolution.y / pixelSize);
   vec2 localUv = fract(gridUv) - 0.5;

   // --- 1. Procedural Background Colors ---
   float wave1 = sin(aspectUv.x * 4.0 - time * 0.8);
   float wave2 = cos(aspectUv.y * 3.0 + time * 0.6);
   float combinedWave = smoothstep(-0.8, 0.8, (wave1 + wave2) * 0.5);
   float wavePeach = sin(aspectUv.y * 3.0 - aspectUv.x * 2.0 + time * 0.9) * 0.5 + 0.5;

   vec3 mixColor = mix(colorBlue, colorCyan, combinedWave);
   vec3 pinkTransition = mix(mixColor, colorHotPink, smoothstep(0.1, 0.7, wavePeach));
   mixColor = mix(pinkTransition, colorPeach, smoothstep(0.5, 1.0, wavePeach));
   vec3 lightGridColor = mix(mixColor, vec3(1.0), 0.6);

   // --- 2. ASCII Grid ---
   float boxSize = 0.3 + sin(time * 2.0 + gridUv.x * 0.2 + gridUv.y * 0.2) * 0.1;
   float radius = 0.12;
   float d = length(max(abs(localUv) - (boxSize - radius), 0.0)) - radius;
   float alpha = smoothstep(0.05, 0.0, d);

   // --- 3. Interactive Organic Masking ---
   // Parallax radial mask: center shifts slightly towards the mouse
   vec2 radialCenter = mix(vec2(0.5, 0.5), mouse, 0.3);
   float radialMask = smoothstep(0.7, 0.1, distance(screenUv, radialCenter));

   vec2 p = aspectUv * 3.0;

   vec2 mouseAspect = vec2(mouse.x * aspect, mouse.y);
   float mouseDist = distance(aspectUv, mouseAspect);

   // Multi-layered rotated sine waves for blobs
   float noise = 0.0;
   noise += sin(p.x + time * 0.4) * sin(p.y + time * 0.3);
   p *= rotate2d(1.1);
   noise += sin(p.x * 1.5 - time * 0.5) * sin(p.y * 1.5 + time * 0.2);
   p *= rotate2d(2.3);
   noise += sin(p.x * 2.0 + time * 0.3) * sin(p.y * 2.0 - time * 0.4);

   // Dynamic shape morphing instead of space warping (prevents stretched ray artifacts)
   // Creates an organic ripple/push effect that morphs the blobs near the cursor
   noise += cos(mouseDist * 12.0 - time * 3.0) * smoothstep(0.6, 0.0, mouseDist) * 1.2;

   float blobMask = smoothstep(0.0, 1.5, noise);

   // Extra interactive element: reveal ASCII grid brightly right at the cursor
   float cursorHighlight = smoothstep(0.2, 0.0, mouseDist);

   float finalMask = clamp((radialMask * blobMask) + (cursorHighlight * 0.5), 0.0, 1.0);
   float finalAlpha = alpha * finalMask * 0.85;

   vec3 finalColor = mix(bgColor, lightGridColor, finalAlpha);
   gl_FragColor = vec4(finalColor, 1.0);
}
```

### B. The 3D model + holographic ASCII shader

Load via GLTFLoader + DRACOLoader (DRACO decoder from gstatic):

```js
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load('ASSET(model.glb)', (gltf) => { /* … */ });
```

On load (verbatim logic): auto-center+scale the model so its max dimension becomes 5 units
(`scale = 5 / maxDim`, save `window.baseModelScale = scale`), re-center via the scaled bounding box,
then override placement:

```js
model.position.set(0.1, -3.93, 4.17);
model.rotation.set(-0.05026, -0.92781, -0.12566);
window.mainModel = model;
window.mainModel.visible = false; // hidden until preloader finishes

camera.position.set(-1.8, -1.0, 6.5);
camera.lookAt(-1.8, 0.5, 0);
controls.target.set(-1.8, 0.5, 0);
controls.update();
```

Every mesh gets a `MeshPhysicalMaterial` and the holographic onBeforeCompile shader patch:

```js
const baseMat = new THREE.MeshPhysicalMaterial({
  color: 0x010103, metalness: 0.9, roughness: 0.2, clearcoat: 1.0, clearcoatRoughness: 0.1,
  transparent: true, opacity: 0.95, side: THREE.DoubleSide
});
window.applyHolographicShader(baseMat);
child.material = baseMat;
```

After adding the model to `scene`, save `window.mainModelMaterial` (one mesh's material) for the
fade logic. Set up the animation mixer if `gltf.animations.length > 0` (play every clip).

`window.applyHolographicShader` injects uniforms (all the shared ones above) and patches the
material's GLSL. Vertex patch: prepend `varying vec3 vLocalPosition;` and after
`#include <begin_vertex>` add (verbatim):

```glsl
#ifdef USE_INSTANCING
  vLocalPosition = (instanceMatrix * vec4(position, 1.0)).xyz;
#else
  vLocalPosition = position;
#endif
```

Fragment patch: prepend the uniform/varying block, then replace `#include <dithering_fragment>`
with `#include <dithering_fragment>` followed by this block (verbatim):

```glsl
vec3 vDir = normalize( vViewPosition );
float fresnel = 1.0 - max(dot(vDir, normal), 0.0);
float fresnelPow = pow(fresnel, 2.0); // Softer fresnel falloff

// Procedural waves (softer, slower)
// Bind to local position so waves stick to the geometry during rotation
float wave1 = sin(vLocalPosition.x * 2.0 - time * 1.0);
float wave2 = cos(vLocalPosition.y * 1.5 + time * 0.8);
float wave3 = sin(vLocalPosition.z * 2.5 + time * 0.5);
float combinedWave = (wave1 + wave2 + wave3) / 3.0;
combinedWave = smoothstep(-0.5, 0.5, combinedWave); // Softer mixing

float wavePeachRaw = sin(vLocalPosition.y * 2.0 - vLocalPosition.x * 1.0 + time * 1.2);
// Normalize to 0..1 for clean blending
float wavePeach = wavePeachRaw * 0.5 + 0.5;

// 1. Base cold colors (Blue -> Cyan)
vec3 mixColor = mix(colorBlue, colorCyan, combinedWave);

// 2. Clean transition to warm colors
// Smooth, gentle transitions between the pastel tones
vec3 pinkTransition = mix(mixColor, colorHotPink, smoothstep(0.2, 0.8, wavePeach));
mixColor = mix(pinkTransition, colorPeach, smoothstep(0.5, 0.95, wavePeach));

// 3. Purple rim light
mixColor = mix(mixColor, colorPurple, fresnel);

// Glowing rim effect (softer)
vec3 glow = colorCyan * fresnelPow * 1.5;

// Curved, softer lines
// Add distortion using X and Z to make the lines wavy/curved
float distortion = sin(vLocalPosition.x * 2.5 + time * 0.5) * 0.4 + cos(vLocalPosition.z * 2.0) * 0.4;
float curvedY = vLocalPosition.y + distortion;

// Less quantity (multiplier 1.2), slower movement
float lines = fract((curvedY - time * 0.2) * 1.2);

// Very soft transition for the lines
float lineIntensity = smoothstep(0.2, 0.6, lines) * smoothstep(1.0, 0.6, lines);
vec3 lineGlow = vec3(0.2, 0.8, 1.0) * lineIntensity * (0.3 + fresnel * 1.0);

vec3 finalEmission = mixColor * 0.6 + glow + lineGlow;

// --- ASCII Dynamic Shapes Effect ---
float pixelSize = 8.0;
vec2 localUv = fract(gl_FragCoord.xy / pixelSize) - 0.5;

float luma = dot(finalEmission, vec3(0.2126, 0.7152, 0.0722));

// 1. Dynamic Size: scale dramatically based on brightness
float currentSize = clamp(luma * 1.2, 0.05, 0.45);

// 2. Dynamic Shape: morph from square to circle based on color!
// wavePeach controls the warm colors.
// 0.0 (Cold blue/cyan) = Sharp Squares
// 1.0 (Warm pink/peach) = Perfect Circles
float currentRadius = mix(0.0, currentSize, wavePeach);

// SDF for dynamic rounded box
float d = length(max(abs(localUv) - (currentSize - currentRadius), 0.0)) - currentRadius;
float shapeAlpha = smoothstep(1.5 / pixelSize, 0.0, d);

// Keep discard for the ASCII grid so depth sorting works correctly
if (shapeAlpha < 0.1) discard;

// Soften/blur the geometric edges of the model using the fresnel angle
// Dynamic blur controlled by the slider (0.0 = sharp, 1.0 = extreme blur)
float edgeStart = mix(1.0, 0.7, edgeBlurAmount);
float edgeEnd = mix(0.99, 0.1, edgeBlurAmount);
float edgeBlur = smoothstep(edgeStart, edgeEnd, fresnel);

finalEmission *= 1.2; // Boost brightness a bit

// --- SPECTACULAR 2D SCREEN-SPACE REVEAL ---
// We use screen coordinates so the reveal is perfectly consistent
// regardless of how the 3D model is scaled or offset internally!
vec2 screenUv = gl_FragCoord.xy / resolution.xy;
screenUv.x *= resolution.x / resolution.y; // correct aspect ratio

// Only carve out the reveal DURING the intro animation. Once it completes the discard is
// fully disabled, so the model can NEVER be hidden again no matter where it flies during
// the scroll choreography (fixes the model vanishing when scrolling into the finale).
if (uRevealProgress < 0.999) {
  // Reveal emanates from the model's ACTUAL on-screen center (updated every frame on the CPU).
  vec2 revealOrigin = uRevealOrigin;
  float distFromCenter2D = distance(screenUv, revealOrigin);

  // Expand radius generously so the whole model is guaranteed to be revealed by the end.
  float currentRadius2D = uRevealProgress * 3.0;

  // Add digital grid noise based on pixel coordinates for a dissolving edge.
  float gridRandom = fract(sin(dot(floor(gl_FragCoord.xy / 8.0), vec2(12.9898, 78.233))) * 43758.5453);
  // Fade the noise out as the reveal completes so nothing stays permanently carved away.
  float noisyRadius2D = currentRadius2D - gridRandom * 0.15 * (1.0 - uRevealProgress);

  if (distFromCenter2D > noisyRadius2D) {
    discard;
  }

  // Glowing edge at the expansion border
  float revealEdge = smoothstep(noisyRadius2D - 0.05, noisyRadius2D, distFromCenter2D);
  vec3 revealGlow = colorHotPink * revealEdge * 3.0 + colorCyan * pow(revealEdge, 4.0) * 8.0;
  finalEmission += revealGlow;
}

// Multiply alpha by edgeBlur to make the silhouette soft and blurry
gl_FragColor = vec4(gl_FragColor.rgb + finalEmission, gl_FragColor.a * shapeAlpha * edgeBlur);
```

The full `applyHolographicShader` skeleton (verbatim structure):

```js
window.applyHolographicShader = function(mat) {
  mat.onBeforeCompile = (shader) => {
    shader.uniforms.time = shaderUniforms.time;
    shader.uniforms.colorCyan = shaderUniforms.colorCyan;
    shader.uniforms.colorPurple = shaderUniforms.colorPurple;
    shader.uniforms.colorBlue = shaderUniforms.colorBlue;
    shader.uniforms.colorPeach = shaderUniforms.colorPeach;
    shader.uniforms.colorHotPink = shaderUniforms.colorHotPink;
    shader.uniforms.edgeBlurAmount = shaderUniforms.edgeBlurAmount;
    shader.uniforms.uRevealProgress = shaderUniforms.uRevealProgress;
    shader.uniforms.uRevealOrigin = shaderUniforms.uRevealOrigin;
    shader.uniforms.resolution = shaderUniforms.resolution;
    shader.vertexShader = `varying vec3 vLocalPosition;n` + shader.vertexShader;
    shader.vertexShader = shader.vertexShader.replace(`#include <begin_vertex>`, `#include <begin_vertex>n` + /* vertex patch above */);
    shader.fragmentShader = `uniform float time;nuniform float edgeBlurAmount;nuniform float uRevealProgress;nuniform vec2 uRevealOrigin;nuniform vec2 resolution;nuniform vec3 colorCyan;nuniform vec3 colorPurple;nuniform vec3 colorBlue;nuniform vec3 colorPeach;nuniform vec3 colorHotPink;nvarying vec3 vLocalPosition;n` + shader.fragmentShader;
    shader.fragmentShader = shader.fragmentShader.replace(`#include <dithering_fragment>`, `#include <dithering_fragment>n` + /* fragment patch above */);
  };
};
```

### C. Logo particle system (built from logo.svg via canvas alpha sampling)

After the model loads, build a particle `Points` cloud. Load the same logo SVG into an `Image`, draw
it to a `450 × (450*aspect)` canvas, read pixel alpha, and emit points for every pixel with
`alpha > 128`. Construction (verbatim logic):

- `thickness = 0.25`. For each opaque pixel push front `z:+thickness/2` and back `z:-thickness/2`
  points; if it's an edge pixel (any 4-neighbor alpha < 128) push `sidePoints = 3` extra z-layer
  points. `isAmbient: 0.0`.
- Ambient depth points: `ambientCount = 3500`, spread `x = w/2 + (rand-0.5)*w*3.0`,
  `y = h/2 + (rand-0.5)*h*10.0`, `z = (rand-0.5)*20.0`, `isAmbient: 1.0`.
- `logoScale = 5.7 / canvas.width`.
- Per point: jitter `±0.6` X/Y and `±0.05` Z; target = `(pt.x+jx - w/2)*logoScale`,
  `-(pt.y+jy - h/2)*logoScale`, `pt.z+jz`. Random origin = cylinder: `angle = rand*2π`,
  `radius = 25 + rand*25`, `randX = cos(angle)*radius`, `randY = (rand-0.5)*45`,
  `randZ = sin(angle)*radius`. Delay `= rand*0.5 + normalizedDistFromCenter*0.5`.

Buffer attributes: `position`(3), `aRandomPosition`(3), `aDelay`(1), `aIsAmbient`(1).

```js
const particleMat = new THREE.ShaderMaterial({
  uniforms: {
    time: shaderUniforms.time, uProgress: { value: 0.0 }, uScale: { value: 1.0 }, uShatterFade: { value: 0.0 },
    colorCyan: shaderUniforms.colorCyan, colorBlue: shaderUniforms.colorBlue,
    colorPeach: shaderUniforms.colorPeach, colorHotPink: shaderUniforms.colorHotPink
  },
  vertexShader: `...`, fragmentShader: `...`,
  transparent: true, depthWrite: false, blending: THREE.NormalBlending
});
window.logoParticles = new THREE.Points(particleGeo, particleMat);
window.logoParticles.position.set(-0.6, -2.0, -3.0);
window.logoParticles.frustumCulled = false;
window.logoParticles.visible = false;
scene.add(window.logoParticles);
```

Particle vertex shader (verbatim):

```glsl
uniform float time;
uniform float uProgress;
uniform float uScale;
attribute vec3 aRandomPosition;
attribute float aDelay;
attribute float aIsAmbient;
varying vec3 vLocalPosition;
varying float vIsAmbient;

void main() {
  vLocalPosition = position;
  vIsAmbient = aIsAmbient;

  float safeProgress = clamp(uProgress, 0.0, 1.0);

  float startThreshold = aDelay * 0.4;
  float particleProgress = clamp((safeProgress - startThreshold) / (1.0 - startThreshold), 0.0, 1.0);

  // Sharper ease out so they snap firmly into place
  float rushEase = 1.0 - pow(1.0 - particleProgress, 4.0);

  // Clean Spin: rotate the random position around Y axis (Local Space)
  float spin = (1.0 - particleProgress) * 10.0;
  float s = sin(spin);
  float c = cos(spin);

  vec3 spiraledPos = aRandomPosition;
  spiraledPos.x = aRandomPosition.x * c - aRandomPosition.z * s;
  spiraledPos.z = aRandomPosition.x * s + aRandomPosition.z * c;

  // FLATTEN the depth of shattered particles so they stay close to the camera
  // and don't shrink into tiny specks due to 3D perspective!
  spiraledPos.z *= 0.15;

  // Smooth vertical convergence without crazy noise
  spiraledPos.y = mix(spiraledPos.y, position.y, rushEase);

  vec3 currentPos = mix(spiraledPos, position, rushEase);

  // Add continuous slow drift to ambient particles AND shattered logo particles!
  // rushEase is 1.0 when assembled, 0.0 when scattered.
  // So (1.0 - rushEase) makes them drift only when scattered!
  float driftFactor = (aIsAmbient > 0.5) ? 1.0 : (1.0 - rushEase);
  if (driftFactor > 0.01) {
     currentPos.x += sin(time * 0.4 + aRandomPosition.y) * 1.5 * driftFactor;
     currentPos.y += cos(time * 0.3 + aRandomPosition.x) * 1.5 * driftFactor;
     currentPos.z += sin(time * 0.5 + aRandomPosition.z) * 1.5 * driftFactor;
  }

  vec4 mvPosition = modelViewMatrix * vec4(currentPos, 1.0);

  // Expand the base size of shattered logo particles by 4x to counteract any depth shrinking
  float shatterBoost = mix(4.5, 1.0, rushEase);
  float baseSize = (aIsAmbient > 0.5 ? 40.0 : (16.0 * shatterBoost)) * uScale;

  // Point size attenuation with less aggressive clamping
  gl_PointSize = clamp(baseSize / max(0.5, -mvPosition.z), 4.0, 120.0);

  gl_Position = projectionMatrix * mvPosition;
}
```

Particle fragment shader (verbatim):

```glsl
uniform float time;
uniform float uProgress;
uniform float uShatterFade;
uniform vec3 colorCyan;
uniform vec3 colorBlue;
uniform vec3 colorPeach;
uniform vec3 colorHotPink;
varying vec3 vLocalPosition;
varying float vIsAmbient;

void main() {
  vec2 cxy = 2.0 * gl_PointCoord - 1.0;
  float r = dot(cxy, cxy);
  if (r > 1.0) discard;

  float dist = sqrt(r);
  // Stronger core, softer edge for a denser "juicy" look without additive washout
  float glow = smoothstep(1.0, 0.2, dist);

  // Spatial Gradient + Flowing Time Animation
  float waveX = sin(time * 1.5) * 1.5;
  float waveY = cos(time * 1.2) * 0.8;

  // Map ambient particles into the gradient bounds so they shimmer too
  float effX = vIsAmbient > 0.5 ? mod(vLocalPosition.x + time, 6.0) - 3.0 : vLocalPosition.x;
  float effY = vIsAmbient > 0.5 ? mod(vLocalPosition.y + time, 2.0) - 1.0 : vLocalPosition.y;

  float tX = smoothstep(-3.0, 3.0, effX + waveX);
  float tY = smoothstep(-1.0, 1.0, effY + waveY);

  // Bilinear blend of 4 colors
  vec3 leftColor = mix(colorBlue, colorCyan, tY);
  vec3 rightColor = mix(colorHotPink, colorPeach, tY);

  // Boost the vibrancy of the colors to make them "juicy"
  vec3 finalColor = mix(leftColor, rightColor, tX) * 1.4;

  // Fade out logo particles during the final shatter phase, but keep ambient particles fully visible
  float alphaMultiplier = vIsAmbient > 0.5 ? 1.0 : (1.0 - uShatterFade);

  // Global fade-in to prevent abrupt popping when they first appear
  float globalFadeIn = smoothstep(0.0, 0.15, uProgress);

  gl_FragColor = vec4(finalColor, glow * alphaMultiplier * globalFadeIn);
}
```

## The loader / reveal

### Preloader blob mesh (in `scene`, drawn by foreground renderer)

A `PlaneGeometry(4,4)` mesh with a wobbling-blob shader, positioned to sit exactly over the model's
spot and face the camera:

```js
const preloaderPointsMat = new THREE.ShaderMaterial({
  uniforms: {
    time: shaderUniforms.time, colorCyan: shaderUniforms.colorCyan, colorPurple: shaderUniforms.colorPurple,
    colorBlue: shaderUniforms.colorBlue, colorPeach: shaderUniforms.colorPeach, colorHotPink: shaderUniforms.colorHotPink,
    uShatter: { value: 0.0 }
  },
  vertexShader: `...`, fragmentShader: `...`, transparent: true, depthWrite: false
});
const preloaderPoints = new THREE.Mesh(new THREE.PlaneGeometry(4,4), preloaderPointsMat);
preloaderPoints.position.set(-1.8, -0.25, 4.5);
preloaderPoints.lookAt(-1.8, -1.0, 6.5);
scene.add(preloaderPoints);
```

Preloader vertex shader (verbatim):

```glsl
uniform float uShatter;
varying vec2 vUv;
void main() {
  vUv = uv;
  vec3 pos = position;
  // Expand while scattering
  pos *= (1.0 + uShatter * 2.0);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
```

Preloader fragment shader (verbatim):

```glsl
uniform float time;
uniform float uShatter;
uniform vec3 colorCyan;
uniform vec3 colorPurple;
uniform vec3 colorBlue;
uniform vec3 colorPeach;
uniform vec3 colorHotPink;
varying vec2 vUv;

void main() {
  vec2 center = vec2(0.5, 0.5);
  vec2 st = vUv - center;

  float angle = atan(st.y, st.x);
  float radius = length(st);

  // Dynamic fluid wobble
  float wobble = sin(angle * 3.0 + time * 1.5) * 0.03 + cos(angle * 2.0 - time) * 0.04;
  float maxRadius = 0.35 + wobble;

  // Scatter/Expand logic
  float currentRadius = maxRadius * (1.0 + uShatter * 1.5);

  // Soft alpha edge
  float alpha = smoothstep(currentRadius + 0.08, currentRadius - 0.02, radius);

  // Dissolve noise during shatter
  float shatterNoise = fract(sin(dot(vUv * 10.0, vec2(12.9898, 78.233))) * 43758.5453);
  alpha -= (shatterNoise * uShatter * 2.0);
  alpha *= (1.0 - uShatter); // Fade out completely

  if (alpha <= 0.01) discard;

  // Organic color gradients
  vec2 p = vUv * 2.0 - 1.0;
  float wave1 = sin(p.x * 2.0 - time * 1.0);
  float wave2 = cos(p.y * 1.5 + time * 0.8);
  float combinedWave = smoothstep(-0.5, 0.5, (wave1 + wave2)/2.0);

  float wavePeach = sin(p.y * 2.0 - p.x * 1.0 + time * 1.2) * 0.5 + 0.5;

  vec3 mixColor = mix(colorBlue, colorCyan, combinedWave);
  vec3 pinkTransition = mix(mixColor, colorHotPink, smoothstep(0.2, 0.8, wavePeach));
  mixColor = mix(pinkTransition, colorPeach, smoothstep(0.5, 0.95, wavePeach));

  // Purple edge
  mixColor = mix(mixColor, colorPurple, smoothstep(0.2, 0.45, radius));
  // Soften colors by mixing towards white
  mixColor = mix(mixColor, vec3(1.0), 0.35); // 35% lighter/pastel

  gl_FragColor = vec4(mixColor, alpha);
}
```

### Odometer numbers & stepped loader

Build the odometer column: for `i = 0..100`, a `4rem`-tall centered div containing `String(i).padStart(3,'0')`,
all injected into `#odometer-numbers`. Stepped target sequence:

```js
const loaderSteps = [25, 50, 75, 100];
let loaderStepIndex = 0;
function advanceLoader() {
  if (loaderStepIndex >= loaderSteps.length) return;
  preloaderTargetPercent = loaderSteps[loaderStepIndex];
  loaderStepIndex++;
  setTimeout(advanceLoader, 300);
}
setTimeout(advanceLoader, 200);
```

State vars: `preloaderDone=false`, `preloaderCurrentPercent=0`, `preloaderTargetPercent=0`,
`preloaderShatter=0.0`.

### Hero title split

`initTitles()` (run on DOMContentLoaded or immediately) walks `#animated-hero-title` and every
`.animated-title`, splitting their text nodes into per-character `<span class="char-mask"><span
class="hero-char">X</span></span>`, preserving `<br>` and inner styled spans. Whitespace stays as
text nodes. An `IntersectionObserver` (`threshold:0.3`) reveals chars of any `.animated-title.flow-title`
when it scrolls into view, staggering `.revealed` at `index*10`ms. A `triggerTextReveal(selector)`
helper (guarded by a `Set` so each fires once) reveals chars for the phase titles that are driven by
scroll progress.

### Preloader → reveal sequence (inside the animate loop, verbatim)

```js
if (!preloaderDone) {
  if (preloaderCurrentPercent < preloaderTargetPercent) {
    preloaderCurrentPercent += delta * 180;
    if (preloaderCurrentPercent > preloaderTargetPercent) preloaderCurrentPercent = preloaderTargetPercent;
    if (odoNumbers) odoNumbers.style.transform = `translateY(-${preloaderCurrentPercent * 4}rem)`;
  }
  if (preloaderCurrentPercent >= 100) {
    preloaderShatter += delta * 2.0;
    const easeInShatter = Math.pow(preloaderShatter, 3.0);
    preloaderPointsMat.uniforms.uShatter.value = easeInShatter;
    if (preloaderShatter > 1.0) {
      preloaderDone = true;
      preloaderPoints.visible = false;
      if (window.mainModel) window.mainModel.visible = true;
      const preloaderHTML = document.getElementById('preloader');
      if (preloaderHTML) preloaderHTML.style.opacity = '0';
      document.querySelectorAll('.preload-hidden').forEach(el => el.classList.remove('preload-hidden'));
      const chars = document.querySelectorAll('#animated-hero-title .hero-char');
      setTimeout(() => {
        chars.forEach((char, index) => setTimeout(() => char.classList.add('revealed'), index * 25));
      }, 150);
    }
  }
  window.scrollTo(0, 0); // lock scroll during preload
  scrollProgress = 0;
}
```

The model reveal circle animates only after the preloader finishes:

```js
if (preloaderDone && shaderUniforms.uRevealProgress.value < 0.999) {
  if (!window.revealStartTime) window.revealStartTime = shaderUniforms.time.value + 0.5;
  const elapsed = shaderUniforms.time.value - window.revealStartTime;
  if (elapsed > 0) {
    let progress = Math.min(1.0, elapsed / 2.3);
    shaderUniforms.uRevealProgress.value = 1 - Math.pow(1 - progress, 3);
  }
}
```

## Scroll choreography & animation loop

### Scroll progress mapping (verbatim)

```js
let scrollProgress = 0;
let _ratio = window.innerWidth / 1200;
let responsiveScale = _ratio <= 1.0 ? _ratio : 1.0 + (_ratio - 1.0) * 0.4;

window.addEventListener('scroll', () => {
  const maxScroll = document.body.scrollHeight - window.innerHeight;
  const pauseScroll = window.innerHeight * 1; // reserve last 100vh as a 100% pause
  const animationMaxScroll = Math.max(1, maxScroll - pauseScroll);
  scrollProgress = window.scrollY / animationMaxScroll;
  scrollProgress = Math.max(0, Math.min(scrollProgress, 1.0));
});
```

### Phase windows (all derived from `scrollProgress`, verbatim)

```js
const transitionProgress = Math.min(1.0, scrollProgress / 0.12);                       // hero → model in frame
const phase3Progress     = Math.max(0, Math.min(1.0, (scrollProgress - 0.40) / 0.14)); // glass slides up
const phase4Progress     = Math.max(0, Math.min(1.0, (scrollProgress - 0.54) / 0.14)); // glass leaves, split feature
const phase5Progress     = Math.max(0, Math.min(1.0, (scrollProgress - 0.68) / 0.12)); // camera dives, particles assemble
const modelFlyInProgress = Math.max(0, Math.min(1.0, (scrollProgress - 0.86) / 0.14)); // model returns
const logoShatterProgress= Math.max(0, Math.min(1.0, (scrollProgress - 0.86) / 0.14)); // logo shatters
```

### Camera path (verbatim)

```js
const targetCamX   = THREE.MathUtils.lerp(-1.8, -0.6, transitionProgress);
const targetFocusX = THREE.MathUtils.lerp(-1.8, -0.6, transitionProgress);
const targetCamY   = THREE.MathUtils.lerp(-1.0, -2.0, transitionProgress);
const targetFocusY = THREE.MathUtils.lerp(0.5, -2.0, transitionProgress);
const phase3CamZ   = THREE.MathUtils.lerp(7.5, 7.15, phase3Progress);
const targetCamZ   = THREE.MathUtils.lerp(6.5, phase3CamZ, transitionProgress);
const finalCamZ    = THREE.MathUtils.lerp(targetCamZ, 0.5, phase5Progress); // deep push
const snapLerp = (current, target, speed) => {
  if (Math.abs(target - current) < 0.0001) return target;
  return THREE.MathUtils.lerp(current, target, speed);
};
const lerpSpeed = delta * 6.0;
// apply: camera.position.{x,y,z} = snapLerp(..., {targetCamX,targetCamY,finalCamZ}, lerpSpeed);
//        controls.target.{x,y} = snapLerp(..., {targetFocusX,targetFocusY}, lerpSpeed);
```

### Model path (verbatim, inside `if (window.mainModel)`)

```js
const baseModelX = THREE.MathUtils.lerp(-0.6, -0.5, transitionProgress);
const baseModelY = THREE.MathUtils.lerp(-3.93, -6.15, transitionProgress);
const baseModelZ = THREE.MathUtils.lerp(4.17, 7.0, transitionProgress);
const targetModelX = THREE.MathUtils.lerp(baseModelX, guiParams.phase4X, phase4Progress);
const targetModelY = THREE.MathUtils.lerp(baseModelY, guiParams.phase4Y, phase4Progress);
const targetModelZ = THREE.MathUtils.lerp(baseModelZ, guiParams.phase4Z, phase4Progress);
const p6Ease = 1.0 - Math.pow(1.0 - modelFlyInProgress, 3);
const p6ModelX = THREE.MathUtils.lerp(targetModelX, -0.4, p6Ease);
const p6ModelY = THREE.MathUtils.lerp(targetModelY, -5.2, p6Ease);
const p6ModelZ = THREE.MathUtils.lerp(targetModelZ, -1.0, p6Ease);

const pointerX = targetMouse.x * 2 - 1, pointerY = targetMouse.y * 2 - 1;
const mouseRotX = pointerY * 0.05, mouseRotY = pointerX * 0.10;
const rotProgress = Math.max(0, Math.min(1.0, (scrollProgress - 0.12) / 0.28));

const targetModelRotX = THREE.MathUtils.lerp(-0.05026, -0.2, transitionProgress);
const finalRotX = THREE.MathUtils.lerp(targetModelRotX, guiParams.phase4RotX, phase4Progress);
const p6RotX = THREE.MathUtils.lerp(finalRotX, 0.0 + mouseRotX, p6Ease);

const initialRotY = -0.92781;
const transitionRotY = THREE.MathUtils.lerp(initialRotY, initialRotY + Math.PI, transitionProgress);
const baseRotY = THREE.MathUtils.lerp(transitionRotY, Math.PI * 2, rotProgress);
const finalRotY = THREE.MathUtils.lerp(baseRotY, guiParams.phase4RotY, phase4Progress);
const p6RotY = THREE.MathUtils.lerp(finalRotY, 0.0 + mouseRotY, p6Ease);

const targetModelRotZ = THREE.MathUtils.lerp(-0.12566, 0.0, transitionProgress);
const finalRotZ = THREE.MathUtils.lerp(targetModelRotZ, guiParams.phase4RotZ, phase4Progress);
const p6RotZ = THREE.MathUtils.lerp(finalRotZ, 0.0, p6Ease);
// snapLerp position & rotation toward p6* values.

const baseScale = window.baseModelScale || 1.0;
const finalScale = baseScale * guiParams.phase4Scale;
const targetScale = THREE.MathUtils.lerp(baseScale, finalScale, phase4Progress);
if (!window.currentModelScale) window.currentModelScale = baseScale;
window.currentModelScale = snapLerp(window.currentModelScale, targetScale, lerpSpeed);
window.mainModel.scale.setScalar(window.currentModelScale);

// Opacity: fade out in phase 5, fade back in during model fly-in
let targetOpacity = 0.95;
if (modelFlyInProgress > 0) targetOpacity = THREE.MathUtils.lerp(0.0, 0.95, Math.min(1.0, modelFlyInProgress * 2.0));
else if (phase5Progress > 0) targetOpacity = THREE.MathUtils.lerp(0.95, 0.0, Math.min(1.0, phase5Progress * 2.0));
if (window.mainModelMaterial) window.mainModelMaterial.opacity = targetOpacity;

// Keep the intro reveal centered on the model's real screen position
if (shaderUniforms.uRevealProgress.value < 0.999) {
  window.mainModel.getWorldPosition(_revealVec);
  _revealVec.project(camera);
  shaderUniforms.uRevealOrigin.value.set(
    (_revealVec.x * 0.5 + 0.5) * (window.innerWidth / window.innerHeight),
    _revealVec.y * 0.5 + 0.5
  );
}
```

### Logo particle animation (verbatim, inside `if (window.logoParticles)`)

When `phase5Progress > 0`: make visible; `targetProgress = phase5Progress`, `targetFade = 0`. During
shatter (`logoShatterProgress > 0`): `explodeEase = THREE.MathUtils.smoothstep(logoShatterProgress,0,1)`,
`targetProgress = 1 - Math.pow(explodeEase, 0.3)` (counteracts the vertex `pow(4)` assembly curve so
the explosion is buttery), keep `targetFade = 0` (never fully fade). snapLerp `uProgress` and
`uShatterFade` at `lerpSpeed*1.5`. Mouse rotation: `rotX = pointerY*0.15*phase5Progress`,
`rotY = pointerX*0.25*phase5Progress`. Scale: `p6Ease = smoothstep(modelFlyInProgress,0,1)`,
`targetScale = lerp(1.0,1.4,p6Ease) * responsiveScale`, snapLerp the scale and mirror it into
`uScale`. Else hidden, reset `uProgress=0`, rotation 0, scale 1, `uScale=1`.

### Flying words (verbatim driver)

```js
updateWordAnimation(word1, scrollProgress, 0.14, 0.26);
updateWordAnimation(word2, scrollProgress, 0.22, 0.34);
updateWordAnimation(word3, scrollProgress, 0.30, 0.40, true); // word3 stops at center then flies up
```

`updateWordAnimation(el, p, start, end, stopAtCenter=false)` (verbatim — reproduce exactly):

```js
function updateWordAnimation(el, p, start, end, stopAtCenter = false) {
  if (!el) return;
  if (p <= start) { el.style.opacity = '0'; return; }
  let localP = (p - start) / (end - start);
  if (!stopAtCenter && localP >= 1.0) { el.style.opacity = '0'; return; }
  let xOff, yOff, rotY, zOff;
  if (stopAtCenter && localP > 1.0) {
    const extraP = Math.min(1.0, (localP - 1.0) / 1.25);
    const easeIn = extraP * extraP;
    xOff = 0; yOff = 0 - (easeIn * 100); rotY = 0; zOff = -800;
    el.style.opacity = '1';
    if (el.children.length > 0) {
      const speedMultipliers = [0.8, 1.4, 0.5, 1.7, 0.9, 1.2];
      for (let i = 0; i < el.children.length; i++) {
        const child = el.children[i];
        const speed = speedMultipliers[i % speedMultipliers.length];
        const extraY = -(easeIn * 60 * speed);
        const blurAmount = easeIn * 24 * speed;
        child.style.transform = `translate3d(0, ${extraY}vh, 0)`;
        child.style.opacity = '1';
        child.style.filter = blurAmount > 0.1 ? `blur(${blurAmount}px)` : 'none';
      }
    }
  } else {
    const sineInOut = (t) => 0.5 - Math.cos(t * Math.PI) / 2;
    if (stopAtCenter) {
      xOff = 1400 - sineInOut(localP) * 1400;
      const yEaseOut = 1 - Math.pow(1 - localP, 3);
      yOff = 80 - (yEaseOut * 80);
    } else {
      xOff = 1400 - sineInOut(localP) * 3100;
      const yProgress = Math.min(1.0, localP / 0.55);
      const yEaseOut = 1 - Math.pow(1 - yProgress, 3);
      yOff = 80 - (yEaseOut * 80);
    }
    rotY = -(xOff / 800) * 60;
    zOff = -800;
    if (el.children.length > 0) {
      const totalChars = el.children.length;
      for (let i = 0; i < totalChars; i++) {
        const child = el.children[i];
        const delay = (i / totalChars) * 0.25;
        const flyInDuration = 0.35;
        let childP = (localP - delay) / flyInDuration;
        childP = Math.max(0, Math.min(1.0, childP));
        let childOpacity = Math.min(1.0, childP / 0.5);
        const yEase = 1 - Math.pow(1 - childP, 3);
        let childY = 60 - (yEase * 60);
        let childBlur = 24 - (yEase * 24);
        if (!stopAtCenter) {
          const outDelay = (i / totalChars) * 0.15;
          let outP = (localP - (0.50 + outDelay)) / 0.25;
          outP = Math.max(0, Math.min(1.0, outP));
          if (outP > 0) {
            const outEase = outP * outP;
            childY -= outEase * 60;
            childBlur += outEase * 24;
            childOpacity = Math.min(childOpacity, 1.0 - outP);
          }
        }
        child.style.transform = `translate3d(0, ${childY}vh, 0)`;
        child.style.opacity = childOpacity.toString();
        child.style.filter = childBlur > 0.1 ? `blur(${childBlur}px)` : 'none';
      }
      el.style.opacity = '1';
    } else {
      let opacity = 1.0;
      if (localP < 0.1) opacity = localP / 0.1;
      else if (!stopAtCenter && localP > 0.8) opacity = 1.0 - (localP - 0.8) / 0.2;
      el.style.opacity = opacity.toString();
    }
  }
  el.style.transform = `translate3d(calc(-50% + ${xOff}px), calc(-50% + ${yOff}vh), ${zOff}px) rotateY(${rotY}deg)`;
}
```

### Glass stats panel (verbatim)

```js
if (glassContainer) {
  const glassGradient = document.getElementById('glass-gradient');
  if (scrollProgress <= 0.54) {
    const easeIn = phase3Progress * phase3Progress;
    const glassY = 100 - (easeIn * 100);
    glassContainer.style.transform = `translateY(${glassY}vh)`;
    if (glassY < 60) triggerTextReveal('#phase3-title');
    if (glassGradient) {
      const currentScale = 0.65 + (0.35 * phase3Progress);
      glassGradient.style.transform = `translate(-50%, -50%) scale(${currentScale})`;
      glassGradient.style.borderRadius = `${phase3Progress * 24}px`;
    }
  } else {
    const easeOut = 1 - Math.pow(1 - phase4Progress, 3);
    glassContainer.style.transform = `translateY(${-(easeOut * 100)}vh)`;
    if (glassGradient) { glassGradient.style.transform = `translate(-50%, -50%) scale(1)`; glassGradient.style.borderRadius = `24px`; }
  }
}
```

### Phase 4 / 5 / 6 HTML containers

- **Phase 4** (`#phase4-container`): when `0 < phase4Progress < 1`, fade in over first 20%
  (`opacity=p`, `translateY(50*(1-p))px`), hold, fade out over last 20% (`translateY(-50*p)px`);
  `pointerEvents='auto'` when opacity>0.5; `triggerTextReveal('#phase4-title')` when opacity>0.7.
  Else opacity 0.
- **Phase 5** (`#phase5-container`): opacity ramp keyed on raw `scrollProgress` — 0 until 0.74,
  fade in 0.74→0.78, hold 0.78→0.82, fade out 0.82→0.86; reveal `#phase5-title-1` & `#phase5-title-2`
  when opacity>0.7.
- **Phase 6** (`#phase6-container` + `#phase6-footer`): from `scrollProgress ≥ 0.86`,
  `opacity = (scrollProgress-0.86)/0.14` (clamped); footer slides up:
  `footerY = 100*(1 - (1-Math.pow(1-opacity,3)))` → `translateY(footerY vh)`; container
  `scale = 0.95 + 0.05*opacity`; reveal `#phase6-title` when opacity>0.7.

### Mouse, resize, loop tail (verbatim)

```js
window.addEventListener('mousemove', (event) => {
  targetMouse.x = event.clientX / window.innerWidth;
  targetMouse.y = 1.0 - (event.clientY / window.innerHeight);
});
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  rendererBg.setSize(window.innerWidth, window.innerHeight);
  renderer.setSize(window.innerWidth, window.innerHeight);
  shaderUniforms.resolution.value.set(window.innerWidth, window.innerHeight);
  let _newRatio = window.innerWidth / 1200;
  responsiveScale = _newRatio <= 1.0 ? _newRatio : 1.0 + (_newRatio - 1.0) * 0.4;
});

function animate(time) {
  requestAnimationFrame(animate);
  if (time) lenis.raf(time);
  const delta = clock.getDelta();
  shaderUniforms.time.value += delta;
  preloaderPointsMat.uniforms.time.value = shaderUniforms.time.value;
  shaderUniforms.mouse.value.lerp(targetMouse, delta * 3.0);
  /* … all phase logic above … */
  if (mixer) mixer.update(delta);
  controls.update();
  rendererBg.render(bgScene, camera);
  renderer.render(scene, camera);
}
animate();
```

## Fixed parameters (bake these in)

- Page: `body { height: 2800vh; background:#ffffff }`, `html { font-size: max(12px, 0.833333vw) }`.
- Palette: bg `#f6fafe`, page `#ffffff`, text `#000`; accents blue `#0091ff`, cyan `#00aaff`,
  purple `#8000ff`, hot-pink `#ff0099`, red `#ff0000`. Gradient line stops, in order: those five.
- Font: `'Sarabun'` weights 200 / 400 (+ italic 400).
- Lenis: `duration:1.5`, easing `t => Math.min(1, 1.001 - 2^(-10t))`.
- Camera: `PerspectiveCamera(75, aspect, 0.1, 1000)`; intro pos `(-1.8,-1.0,6.5)`, lookAt `(-1.8,0.5,0)`.
- Lights: ambient `0xffffff @1.0`; dir1 `0xffffff @2.0` at `(5,10,5)`; dir2 `0xffffff @1.0` at `(-5,-5,-5)`.
- Model material: `MeshPhysicalMaterial(color:0x010103, metalness:0.9, roughness:0.2, clearcoat:1.0,
  clearcoatRoughness:0.1, transparent, opacity:0.95, DoubleSide)`; auto-scaled so maxDim → 5 units;
  initial pos `(0.1,-3.93,4.17)`, rot `(-0.05026,-0.92781,-0.12566)`.
- Phase-4 target: pos `(-1.35,-5.4,5.67)`, scale ×`0.85`, rotY `0.6723`. Finale pos `(-0.4,-5.2,-1.0)`.
- Reveal: starts `+0.5s` after preloader, duration `2.3s`, cubic ease-out, radius `progress*3.0`.
- Bg shader: `pixelSize 15.0`, box radius `0.12`, `finalAlpha ×0.85`. Model ASCII: `pixelSize 8.0`,
  `edgeBlurAmount 0.5`.
- Particles: logo canvas `450px` wide, alpha threshold `128`, thickness `0.25`, `logoScale 5.7/width`,
  `ambientCount 3500`, group pos `(-0.6,-2.0,-3.0)`. Point sizes: ambient 40, logo 16 (×shatterBoost
  up to 4.5), clamp `gl_PointSize` 4–120. Finale logo scale `1.0→1.4`.
- DRACO decoder: `https://www.gstatic.com/draco/versioned/decoders/1.5.7/`.
- Marquee: `25s linear infinite`, 22 logos total (two sets of 11), gap `6rem`, grayscale.
- Phase windows: transition `0→0.12`; rotProgress `0.12→0.40`; words `0.14/0.22/0.30`; phase3
  `0.40→0.54`; phase4 `0.54→0.68`; phase5 `0.68→0.80` (+hold to 0.86); finale/shatter `0.86→1.00`;
  last 100vh is a 100% pause.

## Assets

Route every local asset through:
`ASSET_BASE_URL = https://api.getlayers.ai/storage/v1/object/public/public/assets/clarix-281ac6c399/<filename>`

Define `const ASSET_BASE_URL = 'https://api.getlayers.ai/storage/v1/object/public/public/assets/clarix-281ac6c399';`
and replace each `ASSET(name)` placeholder with `` `${ASSET_BASE_URL}/name` ``.

- `logo.svg` — `…/assets/clarix-281ac6c399/logo.svg` — used twice: the hero `<img class="hero-logo">`, and
  loaded into an `Image()` to canvas-sample the IrisAI logo particle cloud.
- `gradient.png` — `…/assets/clarix-281ac6c399/gradient.png` — the colorful gradient image inside the glass
  stats panel (`.glass-gradient-img`, `object-fit:cover; opacity:0.5`).
- `model.glb` — `…/assets/clarix-281ac6c399/model.glb` — the hero 3D model (DRACO-compressed; load with
  DRACOLoader as above).
- Company marquee logos (11 PNGs, in the `companies/` subfolder), each `…/assets/clarix-281ac6c399/companies/<file>`:
  `Discord.png`, `Facebook.png`, `Snapchat.png`, `Spotify.png`, `TikTok.png`, `WhatsApp.png`,
  `YouTube.png`, `dribbble.png`, `ebay.png`, `instagram.png`, `twitter.png`. Render two identical
  sets (22 `<img>` total) so the `-50% → 0%` marquee loop is seamless.
[/hidecontent]
← 返回首页