Build log · Nocturne Studio
Brief
An after-hours agency reel. Aesthetic target is obys.agency and the late-night tier of Awwwards winners — giant kinetic typography, magnetic hover physics, image reveals that track the cursor, and a single blob cursor that morphs to snap onto interactive targets. The reviewer should open this chapter and feel the site pulling at them gently.
Patterns used
grid-page-transition— mosaic wipe on entry (shared with Editorial)split-text-line-reveal— masked line-by-line heading reveal with skewmagnetic-button-hover— CTAs translate toward the pointer with elastic settlekinetic-oversized-type-scroll— 22vw display type scrolls horizontally with vertical scrollimage-hover-reveal-mask— project list reveals a cursor-following imagehover-swap-project-index— sticky preview image swaps on row hovergooey-sticky-cursor— SVG gooey-filter blob cursor that snaps to[data-gooey-target]
Decisions
The display is Anton, the body is Inter
Anton is ubiquitous on kinetic-type sites for a reason — it's narrow, high-contrast, and survives
at 22vw without collapsing. Inter carries the prose where Anton would be shouting. The chapter
tokens live on [data-chapter="studio"] and the palette is near-black paper (#0b0908) with a
warm butter accent (#f6d67a), pulled to evoke film stock.
Magnetic buttons use gsap.quickTo, not per-frame tweens
gsap.quickTo builds a reusable tween and accepts a new target value on each call. On
pointermove we dispatch the outer translate and the inner content translate separately, at
different strengths (outer 0.35, inner 0.35 × 0.4). The inner parallax is what makes the
button feel like a physical object — without it, the magnetic pull reads as a bug.
Line reveals use SplitType, not SplitText (GSAP)
GSAP's SplitText is paid. SplitType is ~4 KB free and does lines/words/chars just as well for
this use. The pattern inserts an overflow: hidden wrapper around each generated line so the
skew-up doesn't bleed into the lines above and below. The fallback (reduced-motion) is "don't
split at all" — reveal instantly.
The kinetic type is scroll-scrubbed, not auto-scrolled
Auto-scrolling marquee exists on every agency site and is a solved problem (see our Editorial
chapter's infinite-marquee-css). Scrubbing horizontal translate against vertical scroll is the
distinct move — it ties typography to the reader's physical motion. gsap.fromTo with
scrollTrigger.scrub: 1 is the whole implementation.
HoverRevealList and HoverSwapProjectIndex are two different patterns, on purpose
The reveal list's preview follows the cursor with a requestAnimationFrame loop lerped at
0.18 — it feels like a handheld lens. The hover-swap index's preview is sticky in a column and
crossfades as different rows light up — more like an art book. Both answer "show me the image
while I read" but with different physical metaphors. The chapter ships both to make the point.
The gooey cursor snaps to declared targets
Elements that want to attract the cursor carry data-gooey-target. On pointerenter the cursor
grows to the element's bounding-box + 16px and rounds its radius to match. Applying the SVG
<feGaussianBlur> + feColorMatrix filter gives it the metaball morph when crossing between
nearby targets. The filter is defined inline in the component so it follows the cursor wherever
it mounts.
Two cursors, no conflict
The Terminal chapter ships its own cursor (the site-wide custom-cursor-multi-state with a
circle + text label). The Studio chapter replaces that with GooeyCursor. Both set
pointer-events: none. To avoid double cursors, the Studio cursor uses mix-blend-mode: difference so it stays visible against both the chapter canvas AND the site-wide cursor that is
still rendered from the root layout. In production I'd prefer to opt-out of the root cursor on
chapters that ship their own; for a showcase, overlapping is fine and actually informative — you
can see both render modes live next to each other.
Tradeoffs rejected
- A WebGL cursor trail. Already on the roadmap for Chapter 06 (Deep Field). Keeping Studio's cursor SVG-filter based keeps the bundle light for readers who stop here.
- Vertical scrolling typography. Tested; read as decorative rather than confident. Horizontal carried more impact.
- Video backgrounds on project cards. Photography is the right medium for this aesthetic; video on every card would drown the imagery in motion.
- A full CMS-backed project index. Each project is hard-coded. Real studios need a CMS; this is a showcase.
Performance notes
- All GSAP work is client-side inside
useEffect, code-split to/studio. - Unsplash images load via
next/imagewith explicitsizesper layout. The hover-swap preview eagerly loads all images (they're small at 360px) so there's no pop-in on hover. - The gooey-filter is GPU-composited. One rAF loop for the cursor follower; all other motion is GSAP-managed.
- Split-text instances revert on unmount so ScrollTrigger doesn't leak stale DOM references.
What I'd change in production
- Commission photography. Unsplash is a placeholder; a real studio's work is the entire pitch.
- Add a "first visit" tutorial (a single pointer pulse on the gooey cursor) so the interaction is legible to a reader who's never seen it before.
- Respect the OS accent color when available (
color-scheme: light darkplusaccentcolorqueries) so Studio's butter accent harmonises with user preference instead of fighting it. - Defer Inter's weight variants — we load the full family and use only two weights.