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.

documentary
food
creators
fashion

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:

Anton — the poster voiceFraunces italic — the poem voiceSpace Mono — the slate voice (metadata, timecode)Space Grotesk — quiet functional copy like this.Caveat — margin notes on the contact sheet

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.

AN

Ahmad Noureddine — Showreel 2026

000