Director’s commentary
The Making
of This Site
every creative and technical decision behind the experience — written so another designer or developer could rebuild it, or make it their own.
01 — Concept
The Dark Room
The site is built around a single idea: you are sitting inside Ahmad’s color-grading suite at 2 A.M. Everything follows from that. Darkness is not a background color — it is the room. Blue is not a brand color — it is the one monitor still glowing. The running timecode in the header, the “Scene 02” slate labels, the shutter page-transitions and the film-strip backstage roll are all furniture from that room.
The homepage is paced like a short film: cold open (preloader), title card (hero), the person (about), the world (territories), a door (archive CTA), the honest part (backstage), and a closing line (contact). Sections alternate between loud and quiet on purpose — motion rests between peaks.
02 — Art direction
Light as Depth
Instead of photography placeholders from stock libraries, the site generates its own footage: a procedural “light field” renderer (components/CineCanvas.tsx) draws graded, breathing volumetric light unique to each category — warm dust for documentary, ember for food, electric violet for creators, cold steel for fashion. Every visual on the site shares one grade: deep blacks, cool shadows, controlled highlights, constant film grain.
03 — Color
One Light Source
The palette is a night exterior lit by a single blue source. The rule: electric blue appears in small doses — hairlines, one word, one dot — never as a fill for large areas.
- Void #060709Base darkness — the theatre
- Coal #0b0d12Overlays, menu, shutters
- Panel #10131bFrames, cards, monitors
- Midnight #0d1424Deep blue gradient floor
- Electric #4c78ffThe key light — accents only
- Glow #8aa9ffSoft highlights, serif moments
- Dust #99a3b5Body copy
- Steel #5f6f88Metadata, quiet labels
- Ink #edf0f6Controlled white headlines
04 — Typography
Poster, Poem, Slate
Three voices, each with a job:
Contrast is the system: massive condensed uppercase against tiny tracked-out mono; a single italic serif word inside a hard headline (“story in motion”). Display type animates through masked line reveals only — no letter scrambling, no bouncing.
05 — Layout
Asymmetry on a 12-column Grid
Every section uses the same 12-column grid but breaks it differently: the about statement sits columns 1–7 with the portrait pushed to 9–12; archive rows stretch full-bleed; project stills deliberately misalign vertically (translate offsets on a regular grid). Ghost numerals and stroked marquee text create depth behind content instead of decorative blobs.
06 — Motion
Choreography, not Effects
GSAP + ScrollTrigger drive everything, with Lenis providing inertia scrolling. The rules the site follows:
- One easing family — power4.out for entrances, power4.inOut for shutters.
- Text enters through masks (yPercent 115 → 0), never opacity-only fades for headlines.
- Scrubbed timelines (hero recede, territories travel) are reserved for spatial storytelling; triggered timelines for arrival moments.
- Loud sections (hero, CTA) are followed by quiet ones (about, contact) — motion has rests.
- Everything checks prefers-reduced-motion and collapses to static, legible layouts.
07 — Scroll
The Horizontal Travel
“Visual Territories” pins the viewport and converts vertical scroll into a lateral camera move across four panels (components/Territories.tsx). The travel distance is derived from the track’s scroll width, so panels can change size freely. Below 1024px — and under reduced motion — the same markup lays out vertically with no pinning: content first, choreography second.
08 — WebGL
A Shader, not a Library
The hero is a single hand-written GLSL fragment shader (components/hero/HeroCanvas.tsx) — fbm fog, an anamorphic blue key light with chromatic split, dust catching the beam, vignette and grain — reacting to the cursor like a camera on a fluid head. No three.js: for one full-screen quad, a 3KB shader beats a 600KB dependency. It caps device pixel ratio at 1.5, drops render resolution automatically when frames run slow, pauses off-screen and on hidden tabs, and falls back to a CSS gradient when WebGL is unavailable.
09 — Video
Players that Respect the Browser
The reusable components/VideoPlayer.tsx renders a poster state with a play affordance; real files play through the native element with controls, playsInlineand no autoplaying audio. Until real footage is supplied, players run the procedural reel so the experience is complete on day one. Recommended delivery: H.264 MP4 at 1080p, ~8 Mbps, plus a poster JPEG per film.
10 — Responsive
Recomposed, not Squeezed
Each breakpoint gets its own composition: the territories travel becomes a vertical editorial stack; the archive index re-flows its grid; the backstage roll swaps desktop drag-physics for native touch scrolling with snap points; the custom cursor and hover previews simply don’t mount on coarse pointers.
11 — Accessibility
Experimental ≠ Exclusive
- Semantic landmarks, one h1 per route, labelled sections.
- Full keyboard support: real links and buttons everywhere, visible electric-blue focus rings, Escape closes overlays.
- The custom cursor is decoration on top of a functioning pointer, and never mounts for touch or reduced-motion users.
- Hover previews are enhancements; every interaction has a click path.
- Procedural canvases are aria-hidden with text equivalents.
12 — Performance
The Budget
- No 3D library, no video files shipped — atmosphere is generated, costing kilobytes.
- Every canvas pauses off-screen (IntersectionObserver) and on hidden tabs.
- Grain is one CSS-animated SVG tile, not a per-frame canvas.
- Canvas DPR capped (1.25 for 2D, 1.5 adaptive for WebGL).
- Fonts subset and swapped via next/font; routes code-split by Next.js automatically.
13 — Structure
Where Things Live
app/ routes (/, /projects, /projects/[category]/[client], /guide) components/ experience systems hero/ WebGL canvas + hero composition archive/ client index + client reel pages providers/ smooth scroll, page transitions CineCanvas procedural cinematic visuals VideoPlayer in-page film player data/ profile.ts ← all personal info & contacts projects.ts ← films, clients, categories
14 — Content
Replacing the Placeholders
Everything editable lives in two files.
// data/profile.ts email: "hello@ahmadnoureddine.example" → real email instagram: "#" → real profile URL portrait: "" → "/portrait.jpg" in /public showreel: "" → "/showreel.mp4" in /public
Any field left empty keeps its art-directed procedural stand-in — nothing on the site ever looks broken while content is pending.
15 — Projects
Adding a Film
// data/projects.ts — append to the projects array
{
title: "New Film",
slug: "new-film", // becomes /projects/new-film
category: "documentary", // documentary | food | creators | fashion
year: 2026,
client: "Client name",
role: "Director / Editor",
runtime: "02:10",
description: "One paragraph, written like a logline.",
video: "/films/new-film.mp4",
poster: "/films/new-film.jpg",
gallery: ["/films/new-film-1.jpg"],
backstage: [],
credits: [{ role: "Direction", name: "Ahmad Noureddine" }],
featured: true,
seed: 97, // any number — flavors the placeholder grade
}The archive, filters, counts, category pages and next-film navigation all update automatically.
16 — Contact
Wiring Real Channels
Contact rows read from data/profile.ts. Replace the "#" hrefs with real URLs (e.g. https://wa.me/961XXXXXXXX) and the masked phone with a real number. The email address feeds both the contact section and the footer.
17 — Deploy
Shipping It
npm run build # production build (Turbopack) npm start # serve locally # or push to GitHub and import on Vercel / Netlify — # zero config needed; set the domain, done.
Before going live: update siteUrl in profile.ts so Open Graph and structured data point at the real domain, and drop a real 1200×630 sharing image into /public.