fix: computer animation positioning

This commit is contained in:
jwinterm
2026-01-20 09:34:14 -05:00
parent 0df4c3d09c
commit 6f8c2ba325
3 changed files with 66 additions and 9 deletions
+51 -2
View File
@@ -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<HTMLHeadingElement>(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 (
<main className="relative flex flex-col items-center justify-between overflow-hidden">
<DynamicBackground />
<DynamicBackground nebulaPosition={nebulaPosition} />
<Header />
<div id="main-content" className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2">
<h1
ref={titleRef}
className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2"
style={{
// Calculated max-width to prevent nebula overlap:
// Monitor right edge = 5% of vw + 28px (half of 56px icon)
// Title left edge for centered text = (vw - titleWidth) / 2
// For no overlap: titleWidth < 0.9 * vw - 56px
// Adding 64px buffer for comfortable spacing
maxWidth: 'min(calc(90vw - 120px), 100%)',
}}
>
Digital Craftsmanship
</h1>
<p className="max-w-2xl text-xl text-muted-foreground mb-8">
@@ -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() {
<NebulaField
className="fixed inset-0 z-0 h-screen w-screen"
themeMode={themeMode}
position={nebulaPosition ?? undefined}
/>
);
}
+9 -6
View File
@@ -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(() => {