diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 3e077d6..6403d6d 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import { PortfolioGrid } from "@/components/portfolio/bento-grid"; import { ContactForm } from "@/components/contact/contact-form"; import { FounderSignature } from "@/components/about/founder-signature"; @@ -6,15 +8,62 @@ import { Header } from "@/components/layout/header"; import { DynamicBackground } from "@/components/landing/dynamic-background"; import Link from "next/link"; import { Button } from "@repo/ui/components/button"; +import { useRef, useState, useEffect } from "react"; export default function Home() { + const titleRef = useRef(null); + const [nebulaPosition, setNebulaPosition] = useState<{ x: number; y: number } | null>(null); + + useEffect(() => { + const updatePosition = () => { + if (titleRef.current) { + const rect = titleRef.current.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Position nebula to the left of the title with a comfortable margin + // Convert pixel position to percentage of viewport + const titleLeftPercent = (rect.left / viewportWidth) * 100; + const titleTopPercent = (rect.top / viewportHeight) * 100; + + // Place the nebula 8% to the left of where the title starts + // and vertically aligned with the title's top area + const nebulaX = Math.max(5, titleLeftPercent - 8); + const nebulaY = Math.max(10, titleTopPercent - 2); + + setNebulaPosition({ x: nebulaX, y: nebulaY }); + } + }; + + updatePosition(); + + // Update on resize + window.addEventListener('resize', updatePosition); + + // Also update after fonts load (can affect title width) + document.fonts?.ready.then(updatePosition); + + return () => window.removeEventListener('resize', updatePosition); + }, []); + return (
- +
-

+

Digital Craftsmanship

diff --git a/apps/web/src/components/landing/dynamic-background.tsx b/apps/web/src/components/landing/dynamic-background.tsx index 02b299f..8c8bed8 100644 --- a/apps/web/src/components/landing/dynamic-background.tsx +++ b/apps/web/src/components/landing/dynamic-background.tsx @@ -13,7 +13,11 @@ const NebulaField = dynamic( } ); -export function DynamicBackground() { +interface DynamicBackgroundProps { + nebulaPosition?: { x: number; y: number } | null; +} + +export function DynamicBackground({ nebulaPosition }: DynamicBackgroundProps) { const { resolvedTheme, systemTheme } = useTheme(); const [mounted, setMounted] = useState(false); const [showNebula, setShowNebula] = useState(false); @@ -51,6 +55,7 @@ export function DynamicBackground() { ); } diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index 5da979e..e1bb057 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -9,6 +9,8 @@ interface NebulaProps { className?: string; density?: number; themeMode?: 'light' | 'dark'; + /** Dynamic position as percentage of viewport. If not provided, uses default fixed position. */ + position?: { x: number; y: number }; } interface StreamLineProps { @@ -77,7 +79,7 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => { ); }; -export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: NebulaProps) => { +export const NebulaField = ({ className, density = 8, themeMode = 'dark', position }: NebulaProps) => { const monitorControls = useAnimation(); const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500/60"); const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); @@ -118,13 +120,14 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu const yRange = useTransform(scrollY, [0, 1000], [0, 250]); // 1. Computer Position: Where the icon sits on the screen (%) - const computerX = 18; - const computerY = 18; + // Use dynamic position if provided, otherwise fall back to defaults + const computerX = position?.x ?? 18; + const computerY = position?.y ?? 18; // 2. Streamer Convergence: Where the streamers actually head (%) - // If streamers hit "up and to the left", increase these values slightly. - const targetX = 19.8; // Tweak this to move streamer hit point right - const targetY = 19.8; // Tweak this to move streamer hit point down + // Offset from computer position so streamers hit the center of the monitor + const targetX = computerX + 2.5; + const targetY = computerY + 2.5; const paths = useMemo(() => { return Array.from({ length: density }).map(() => {