diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index 9b11574..de708bd 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -1,4 +1,7 @@ -import { Activity } from 'lucide-react'; +import { Activity, LaptopMinimalCheck } from 'lucide-react'; +import { RevolvingPrism } from './prism/RevolvingPrism'; +import { HeroTitle } from './HeroTitle'; +import './prism/styles.css'; type HeroSectionProps = { activeThemeName: string; @@ -15,43 +18,82 @@ export function HeroSection({ }: HeroSectionProps) { return (
-
+
+
+
+
+ Operator portrait +
+
+
+
+
{showThemePillTooltip && (
HAHA Made you click
)}
-

- I overdesign - controllable systems - - ,
deconstruct black boxes, and build tools. -

+
+ +
+ +
+ +

Systems engineer in the making. From window managers and game engines to self-hosted infrastructure.

+
+
+ + This looks a lot cooler on bigger screens. +
+
-
-
-
+
+
+
-
+
Operator portrait
-
+
the operator
diff --git a/src/components/HeroTitle.tsx b/src/components/HeroTitle.tsx new file mode 100644 index 0000000..c3219d6 --- /dev/null +++ b/src/components/HeroTitle.tsx @@ -0,0 +1,10 @@ +export function HeroTitle() { + return ( +

+ I overdesign + controllable systems + + ,
deconstruct black boxes, and build tools. +

+ ); +} diff --git a/src/components/prism/RevolvingPrism.tsx b/src/components/prism/RevolvingPrism.tsx new file mode 100644 index 0000000..36d3274 --- /dev/null +++ b/src/components/prism/RevolvingPrism.tsx @@ -0,0 +1,171 @@ +import { useRef, useState, useEffect } from "react"; +import { Activity } from "lucide-react"; + +export function RevolvingPrism() { + const faces = [ + { + id: 0, + title: "01_INFRA", + text: "Personal Infrastructure", + desc: "Self-hosting with Docker, Traefik, Gitea, S3 stacks and Linux. Owning deployment, networking, and storage end-to-end." + }, + { + id: 1, + title: "02_DESKTOP", + text: "Desktop & Workflow Engineering", + desc: "KDE, KWin scripting, Wayland, Tauri, tiling logic, automation. Modifying the environment instead of adapting to it." + }, + { + id: 2, + title: "03_ENGINES", + text: "Simulation & Engine Design", + desc: "TypeScript and Rust. Structured world state, deterministic intent systems, LLM-assisted orchestration." + }, + { + id: 3, + title: "04_HARDWARE", + text: "Hardware & Embedded Experiments", + desc: "ESP32, relays, ACPI quirks, eDP controller boards, audio chains, and soldered prototypes." + } + ]; + + const rotationRef = useRef(0); + const isHoveredRef = useRef(false); + const isDraggingRef = useRef(false); + const lastMouseX = useRef(0); + const [, setRenderTick] = useState(0); + + // animation loop + useEffect(() => { + let animationFrameId: number | null = null; + const loop = () => { + if (!isDraggingRef.current) { + if (isHoveredRef.current) { + // SNAP: Magnetically lerp to the nearest 90-degree angle + const nearest = Math.round(rotationRef.current / 90) * 90; + rotationRef.current += (nearest - rotationRef.current) * 0.08; + } else { + // CONTINUOUS: Rotate slowly (approx 16s per revolution at 60fps) + rotationRef.current -= 0.375; + } + } + // force a re-render to apply inline transform and check active states + setRenderTick(t => t + 1); + animationFrameId = requestAnimationFrame(loop); + }; + animationFrameId = requestAnimationFrame(loop); + return () => { + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); + } + }; + }, []); + + // Drag Interactions + const handlePointerDown = (e: React.PointerEvent) => { + isDraggingRef.current = true; + lastMouseX.current = e.clientX; + e.currentTarget.setPointerCapture(e.pointerId); + }; + + const handlePointerMove = (e: React.PointerEvent) => { + if (!isDraggingRef.current) return; + const delta = e.clientX - lastMouseX.current; + rotationRef.current += delta * 0.8; // Adjust drag sensitivity + lastMouseX.current = e.clientX; + }; + + const handlePointerUp = (e: React.PointerEvent) => { + isDraggingRef.current = false; + e.currentTarget.releasePointerCapture(e.pointerId); + }; + + return ( +
isHoveredRef.current = true} + onMouseLeave={() => { + isHoveredRef.current = false; + isDraggingRef.current = false; + }} + onPointerDown={handlePointerDown} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + onPointerCancel={handlePointerUp} + > + {/* Annotation for hover interaction */} +
+ + [ hover to snap | drag to rotate ] +
+ +
+
+
+
+ + {faces.map((face, index) => { + // Calculate the true world rotation of this specific face + const worldRotation = rotationRef.current + (index * 90); + + // Normalize between -180 and +180 + let normalized = worldRotation % 360; + if (normalized > 180) normalized -= 360; + if (normalized < -180) normalized += 360; + + // Face is considered "Active" if it is within ~42 degrees of perfectly parallel + const isActive = Math.abs(normalized) < 42; + + // Calculate staircase offset for dots and routing line + const dotY = 20 + index * 30; + const dy = index * 30; // Distance between the dot and the text + + return ( +
+ {/* Dynamically assign height to create the staircase */} +
+ +
+ {/* Architectural dynamic routing line bridging the gap */} + + + + +
+
{face.title}
+
{face.text}
+
+
+
+ ); + })} +
+ + {/* Static Descriptions Container */} +
+ {faces.map((face, index) => { + const worldRotation = rotationRef.current + (index * 90); + let normalized = worldRotation % 360; + if (normalized > 180) normalized -= 360; + if (normalized < -180) normalized += 360; + const isActive = Math.abs(normalized) < 42; + + return ( +
+

+ {face.desc} +

+
+ ); + })} +
+
+
+ ); +} diff --git a/src/components/prism/styles.css b/src/components/prism/styles.css new file mode 100644 index 0000000..21cb15e --- /dev/null +++ b/src/components/prism/styles.css @@ -0,0 +1,136 @@ + /* 3D Prism Animation Styles */ + .perspective-1000 { + perspective: 1000px; + } + + .prism-container { + --prism-width: 56px; + --prism-height: 220px; + --prism-depth: 28px; + --prism-cap-size: 56px; + width: var(--prism-width); + height: var(--prism-height); + position: relative; + transform-style: preserve-3d; + } + + .prism-face { + position: absolute; + width: var(--prism-width); + height: var(--prism-height); + background: var(--bg-panel); + border: 2px solid var(--border-main); + transform-style: preserve-3d; + } + + .prism-face-0 { + transform: rotateY(0deg) translateZ(var(--prism-depth)); + } + + .prism-face-1 { + transform: rotateY(90deg) translateZ(var(--prism-depth)); + } + + .prism-face-2 { + transform: rotateY(180deg) translateZ(var(--prism-depth)); + } + + .prism-face-3 { + transform: rotateY(270deg) translateZ(var(--prism-depth)); + } + + .prism-cap { + position: absolute; + left: 0; + top: calc((var(--prism-height) - var(--prism-cap-size)) / 2); + width: var(--prism-cap-size); + height: var(--prism-cap-size); + background: var(--bg-surface); + border: 2px solid var(--border-main); + } + + .prism-cap-top { + transform: rotateX(90deg) translateZ(calc(var(--prism-height) / 2)); + } + + .prism-cap-bottom { + transform: rotateX(-90deg) translateZ(calc(var(--prism-height) / 2)); + } + + .glowing-dot { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--border-main); + transition: all 0.3s ease; + } + + .prism-face.active .glowing-dot { + background: var(--color-accent); + box-shadow: 0 0 8px var(--color-accent), 0 0 16px var(--color-accent); + } + + .prism-popout { + position: absolute; + left: 100%; + top: 32px; + /* Aligns with the fixed height of the text */ + transform: translateY(-50%); + display: flex; + align-items: center; + pointer-events: none; + transform-style: preserve-3d; + } + + /* SVG Line wiping animation */ + .prism-line-svg { + -webkit-mask-image: linear-gradient(to right, #000 40%, transparent 60%); + mask-image: linear-gradient(to right, #000 40%, transparent 60%); + -webkit-mask-size: 300% 100%; + mask-size: 300% 100%; + -webkit-mask-position: 100% 0; + mask-position: 100% 0; + transition: -webkit-mask-position 0.3s ease-out, mask-position 0.3s ease-out; + } + + .prism-face.active .prism-line-svg { + -webkit-mask-position: 0 0; + mask-position: 0 0; + transition: -webkit-mask-position 0.4s cubic-bezier(0.25, 1, 0.5, 1); + } + + .prism-text { + margin-left: var(--prism-width); + /* Reserves space for the absolute-positioned SVG line */ + padding-left: 10px; + transform-style: preserve-3d; + + /* Masking for directional fade (wipes left/right) */ + -webkit-mask-image: linear-gradient(to right, #000 40%, transparent 60%); + mask-image: linear-gradient(to right, #000 40%, transparent 60%); + -webkit-mask-size: 300% 100%; + mask-size: 300% 100%; + -webkit-mask-position: 100% 0; + mask-position: 100% 0; + + opacity: 0; + /* Fade out right-to-left (sped up to clear out earlier) */ + transition: -webkit-mask-position 0.2s ease-out, mask-position 0.2s ease-out, opacity 0s linear 0.2s; + } + + .prism-face.active .prism-text { + -webkit-mask-position: 0 0; + mask-position: 0 0; + opacity: 1; + /* Fade in left-to-right (slight delay lets the line draw first) */ + transition: -webkit-mask-position 0.5s ease-out 0.1s, mask-position 0.5s ease-out 0.1s, opacity 0s linear 0.1s; + } + + /* Simulates 3D extrusion/thickness on flat text */ + .prism-3d-text { + text-shadow: 1px 1px 0 var(--border-main), 2px 2px 0 var(--bg-base); + backface-visibility: visible; + } \ No newline at end of file diff --git a/src/utils.tsx b/src/utils.tsx index e3020fd..afadbab 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,4 +1,4 @@ -function deterministicIpValue(ip: string, x: number): number { +export function deterministicIpValue(ip: string, x: number): number { if (!Number.isInteger(x) || x <= 0) { throw new Error("x must be a positive integer"); }