HOW IT'S MADE · 製作筆記
Kowloon Nights is a love letter to the wet-neon alley — Wong Kar-wai glow, humming transformers, broth that outlived the lease. The brief to myself: an isometric night-market street with zero images and zero WebGL. Every building, window, sign and puddle is a transformed <div>; the only pixels pushed by script are the rain.
The ground is a 1600×1600 plane under rotateX(60deg) rotateZ(-45deg) — the classic 2:1 isometric camera — with transform-style: preserve-3d the whole way down. Each building is three faces: a roof at translateZ(h), and two walls stood up with rotateX(90deg) and rotateZ(90deg) rotateX(90deg) around a ground-edge transform-origin. A seeded PRNG generates the block, so the street is random but identical on every load — useful for screen-recording. Lit windows are Grid cells; roughly one in seven gets a steps(1,end) opacity track with a negative random delay, so the block never blinks in unison. Neon signs use the inverse-camera trick — rotateZ(45deg) rotateX(-60deg) cancels the world's rotation, billboarding each sign toward the viewer while it still sits, and parallaxes, at a true 3D coordinate.
Rain is a single 2D canvas: nearly 200 slanted strokes with depth-graded speed, length and alpha, each carrying its own floor — a random landing line — where it dies into an expanding elliptical splash, faking ground depth for free. Puddles and each sign's reflection are translucent gradients flat on the ground plane; they were mix-blend-mode: screen once, until a blending child inside a preserve-3d context turned out to flatten the entire scene in Chromium — alpha only, now. A slow-drifting, blurred mist layer sits between the buildings and the rain. Steam is five blurred blobs on a staggered rise loop. DPR is capped at 2, and every loop idles when the tab hides or the hero scrolls away.
Authentic neon never fails politely. The buzzing letters (the masthead's final N, OK Electric's C, Midnight Leaf's dot) run a hand-tuned steps(1,end) keyframe track with irregular gaps — three bursts of stutter inside a 3.7s loop reads as electrical fault, not animation. Under prefers-reduced-motion, every flicker locks on, the camera parks, and the rain renders one static frame.
The whole point is a recording that never looks paused, so nothing sits still. Mismatched sine waves drive the world's rotation and the rig's drift so the loop never visibly repeats; the cursor lerps in on top for a parallax that trails, not snaps. Every sign trembles on its own rotate/translate — independent CSS transform properties, composing with the billboard transform JS already owns instead of fighting it. Leaving the hero is scroll-linked: as scrollY advances, the world dollies in and the masthead dissolves, set as inline styles each frame so they never fight the entrance keyframes still holding their children. The directory cards get an IntersectionObserver that staggers each one's arrival past 22% visible. And the pointer gets a cursor of its own inside the hero: a lerped neon ring, hover:hover-gated so touch is untouched, that blooms yellow over the CTA.
Pass one raised the block and got the face math honest — fixed a Chromium preserve-3d sorting bug slicing text off two signs, and recovered two more hiding behind the masthead scrim. Pass two was choreography: the scroll dolly, per-sign sway, staggered cards, heavier rain, mist, and the cursor above — plus a mobile fix, dropping the far-right signs that used to clip the narrow viewport's edge instead of cramming the whole street into one hand's width.