From 3ceeca1be076154647e123744c3bbb60721abaa0 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 3 Nov 2026 12:09:27 +0100 Subject: [PATCH] Scale up hero graphics and fade them into the text - Stage/SceneCanvas: per-breakpoint framing (zoom + horizontal shift) - Desktop: graphic spans 72% of the hero, zoomed ~1.35-1.4x and shifted right, masked to fade toward the text column and at top/bottom edges - Mobile: full-bleed, taller, zoomed ~1.3x; lower part slides under the text and fades out downward Co-Authored-By: Claude Opus 5.5 --- src/components/general/page-hero.tsx | 14 ++++++--- src/components/lib/three/SceneCanvas.tsx | 26 +++++++++++++++- src/components/lib/three/stage.ts | 14 ++++++++- src/components/pages/Home/index.tsx | 13 ++++++-- src/styles/globals.css | 39 ++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/components/general/page-hero.tsx b/src/components/general/page-hero.tsx index a47aa69..99b928d 100644 --- a/src/components/general/page-hero.tsx +++ b/src/components/general/page-hero.tsx @@ -40,14 +40,20 @@ export default function PageHero({ >
- +
{eyebrow && ( diff --git a/src/components/lib/three/SceneCanvas.tsx b/src/components/lib/three/SceneCanvas.tsx index 87dd4ea..79804fb 100644 --- a/src/components/lib/three/SceneCanvas.tsx +++ b/src/components/lib/three/SceneCanvas.tsx @@ -16,12 +16,28 @@ const SCENES = { export type SceneName = keyof typeof SCENES; +export type SceneLayout = { zoom?: number; shiftX?: number }; + type Props = { scene: SceneName; className?: string; + /** Framing at xl (≥1280px) and up. */ + desktop?: SceneLayout; + /** Framing below xl. */ + mobile?: SceneLayout; }; -export default function SceneCanvas({ scene, className }: Props) { +const DESKTOP_QUERY = "(min-width: 1280px)"; + +export default function SceneCanvas({ + scene, + className, + desktop, + mobile, +}: Props) { + // read through a ref so the stage always sees the latest props + const layoutRef = React.useRef({ desktop, mobile }); + layoutRef.current = { desktop, mobile }; const containerRef = React.useRef(null); const canvasRef = React.useRef(null); const [ready, setReady] = React.useState(false); @@ -45,6 +61,14 @@ export default function SceneCanvas({ scene, className }: Props) { canvas, container, factory: mod.default, + layout: () => { + const l = layoutRef.current; + return ( + (window.matchMedia(DESKTOP_QUERY).matches + ? l.desktop + : l.mobile) ?? {} + ); + }, onReady: () => setReady(true), }); if (disposed) handle.dispose(); diff --git a/src/components/lib/three/stage.ts b/src/components/lib/three/stage.ts index c20e0a3..d27e059 100644 --- a/src/components/lib/three/stage.ts +++ b/src/components/lib/three/stage.ts @@ -37,12 +37,20 @@ export type SceneFactory = ( stage: Stage, ) => SceneInstance | Promise; +export type StageLayout = { zoom?: number; shiftX?: number }; + export type StageOptions = { canvas: HTMLCanvasElement; container: HTMLElement; factory: SceneFactory; entryDuration?: number; entryDelay?: number; + /** + * Framing per breakpoint. `zoom` > 1 scales the scene up past its frame + * (it may crop); `shiftX` moves the scene horizontally as a fraction of + * the canvas width (+ = right). Re-evaluated on every resize. + */ + layout?: () => StageLayout; onReady?: () => void; }; @@ -117,8 +125,12 @@ export async function createStage(opts: StageOptions) { const tan = Math.tan(THREE.MathUtils.degToRad(FOV / 2)); const zForHeight = instance.frame.height / 2 / tan; const zForWidth = instance.frame.width / 2 / (tan * camera.aspect); - camera.position.set(0, 0, Math.max(zForHeight, zForWidth)); + const { zoom = 1, shiftX = 0 } = opts.layout?.() ?? {}; + camera.position.set(0, 0, Math.max(zForHeight, zForWidth) / zoom); camera.lookAt(0, 0, 0); + // shift the rendered window so the subject sits off-centre + if (shiftX) camera.setViewOffset(w, h, -shiftX * w, 0, w, h); + else camera.clearViewOffset(); camera.updateProjectionMatrix(); if (reducedMotion) renderFrame(0, 0); } diff --git a/src/components/pages/Home/index.tsx b/src/components/pages/Home/index.tsx index a0ca359..36d7769 100644 --- a/src/components/pages/Home/index.tsx +++ b/src/components/pages/Home/index.tsx @@ -20,12 +20,19 @@ export default function Main() { >
- +
Benjamin Toby diff --git a/src/styles/globals.css b/src/styles/globals.css index dd6c2d5..1895ffd 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -114,3 +114,42 @@ html.motion:not(.motion-fallback) [data-reveal-stagger] > * { padding-bottom: 0.08em; margin-bottom: -0.08em; } + +/* Hero graphics (