Full Page
Deadspace — Agency Portfolio
GSAPThree.jsLenisViteVanilla JS
Previews





About this build
Full agency portfolio with WebGL procedural cityscape shader, 12-block grid preloader with flicker effect, page transitions, WebGL particle system with mouse distortion, SVG pie ring ScrollTrigger animation, and stats slide-in — red/black palette, DM Mono + De Fonte Plus.
Prompt
Build a complete multi-page agency portfolio site called "Deadspace" using Vanilla JS, Vite, GSAP, Three.js, and Lenis. Use the following specifications exactly:
## Color & Typography
- Primary color: #e30613 (red)
- Background: #0a0a0a (near-black)
- Text: #f0f0f0
- Fonts: "De Fonte Plus" for display headings, "DM Mono" for body/mono text, "Stylish" for decorative elements
- Load fonts via @font-face
## Pages
Build 5 pages: index.html, work.html, project.html, lab.html, contact.html
## Navigation
- Fixed nav bar with: time display (left, updated every second), centered logo, location text (right, e.g. "Paris, FR")
- Nav links animate in on page load with staggered opacity/translateY
- On scroll down: nav shrinks height with transition; on scroll up: restores
## Grid Preloader
- Full-screen overlay divided into 12 equal blocks (4 columns × 3 rows)
- On load: blocks flicker in and out with GSAP staggers and random delays (each block: randomized opacity 0→1→0 cycles, 3–5 times)
- Progress bar at bottom: increments randomly (Math.random() * 15 + 5) every 150–300ms until 100%
- When progress hits 100%: all blocks flicker off simultaneously, overlay fades out, page reveals
- Counter number displayed during loading (0–100)
- Store "preloaderSeen" in sessionStorage — skip on return visits
## Page Transitions
- When any nav link is clicked: prevent default navigation
- Trigger same 12-block grid animation to cover the screen (blocks flicker in with randomized order)
- After blocks fully cover: navigate via window.location.href
- On new page load: blocks immediately visible, then flicker out to reveal page
- Store "pageTransition" flag in sessionStorage to coordinate enter/exit states
## WebGL Background — Procedural Cityscape Shader
Using Three.js ShaderMaterial on a full-screen PlaneGeometry (2,2) with OrthographicCamera:
- Vertex shader: standard passthrough with vUv
- Fragment shader: procedural city skyline using a seeded LCG RNG:
- uint seed = 31713U; float rand() { seed = (seed << 13U) ^ seed; seed = seed * (seed * seed * 15731U + 789221U) + 1376312589U; ... }
- Buildings via Polygon() function: random height/width per column using seed
- Building color: dark (#111) against near-black sky (#0a0a0a)
- Layer depth: 8 building layers stacked with decreasing opacity
- Animate: iTime uniform drives slow parallax scroll per layer
- Uniforms: iTime (float), iResolution (vec3)
- Resize handler updates resolution uniform
- pixelRatio capped at 1.25 on desktop, 1.0 on mobile
## WebGL Particle System (Lab page)
- Load source image via THREE.TextureLoader → draw to offscreen canvas → sample pixel data
- Create THREE.Points with ~15,000 particles
- Each particle stores: originalPosition, currentPosition, velocity
- Mouse interaction (desktop only): mousemove event
- Distortion radius: 2000 units; forceStrength: 0.05; returnForce: 0.1; maxDisplacement: 1000
- Particles within radius pushed away proportional to (1 - dist/radius)
- Particles return to originalPosition via lerp each frame
- Blending: THREE.AdditiveBlending, pointSize: 1.5px
- Particle colors sampled from source image pixels
## SVG Pie / Ring Transition
- Large SVG with a donut/ring shape (two concentric circles as clip) centered in viewport
- On scroll (GSAP ScrollTrigger, scrub: true):
- Ring scales from 0.3× to 2.5× (fills then overfills viewport)
- stroke-dashoffset animates from full circumference → 0 (ring draws itself)
- Opacity fades from 1 → 0 at end of scroll range
- Dots scattered around ring edge, also animated with ScrollTrigger
- Text "IDEAS EMERGE" revealed behind ring via clip-path at midpoint of scroll
- SplitText on section header: chars animate in with stagger on scroll
## Stats Section
- 3–4 stat items with large number (80px+, De Fonte Plus) + label (DM Mono, 12px)
- On scroll (ScrollTrigger scrub): each item slides in from right: translateX(250px → 0)
- Trigger: "top bottom" → end: "top 25%"
## Lenis Smooth Scroll
- Initialize: new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), orientation: 'vertical', smoothWheel: true })
- Bridge: gsap.ticker.add((time) => lenis.raf(time * 1000)); gsap.ticker.lagSmoothing(0)
- lenis.on('scroll', ScrollTrigger.update)
## File Structure
```
/
index.html, work.html, project.html, lab.html, contact.html
globals.css
/css: nav.css, preloader.css, index.css, work.css, project.css, lab.css, contact.css, menu.css, footer.css
/js: preloader.js, transition.js, skyline.js, particle-visual.js, pie-transition.js, stats.js, nav.js, menu.js, lenis-scroll.js, footer.js, clients.js, animated-copy.js
/public/fonts: de-fonte-plus.ttf, dm-mono.ttf, stylish.ttf
```
## Technical Notes
- ES modules throughout (type="module")
- All ScrollTrigger animations: respect prefers-reduced-motion via window.matchMedia
- WebGL canvases: position fixed, z-index -1, pointer-events none, width/height 100%
- DPR: Math.min(devicePixelRatio, 1.25) on desktop
- GSAP SplitText used for character-level text animations on section headers
