From 2026e475d5d70b0d76d70cfe89adee8291bafed3 Mon Sep 17 00:00:00 2001 From: Emile Jellinek Date: Sun, 10 May 2026 21:50:39 +0200 Subject: [PATCH] new hero animation for desktop page --- src/components/common/MacbookMockup.tsx | 296 ++++++++++++++++++++++++ src/pages/products/Desktop.tsx | 8 +- 2 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 src/components/common/MacbookMockup.tsx diff --git a/src/components/common/MacbookMockup.tsx b/src/components/common/MacbookMockup.tsx new file mode 100644 index 0000000..19f2633 --- /dev/null +++ b/src/components/common/MacbookMockup.tsx @@ -0,0 +1,296 @@ +import { useRef, useState, useEffect } from 'react' + +interface MacbookMockupProps { + src: string + alt?: string + className?: string +} + +export const MacbookMockup = ({ src, alt = '', className = '' }: MacbookMockupProps) => { + const ref = useRef(null) + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + const el = ref.current + if (!el) return + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setIsOpen(true) + return + } + + const open = () => setTimeout(() => setIsOpen(true), 150) + + const observer = new IntersectionObserver( + ([entry]) => { if (entry.isIntersecting) { open(); observer.disconnect() } }, + { threshold: 0.2, rootMargin: '0px' } + ) + observer.observe(el) + + const rect = el.getBoundingClientRect() + if (rect.top < window.innerHeight && rect.bottom > 0 && rect.height > 0) { + const timer = setTimeout(() => setIsOpen(true), 500) + return () => { clearTimeout(timer); observer.disconnect() } + } + return () => observer.disconnect() + }, []) + + /* + * Geometry (final): + * + * perspective: 2000px → large value = minimal distortion, flat/frontal screen + * perspectiveOrigin: 50% 60% → viewer slightly below center + * + * Stage: rotateX(-12deg) only — NO rotateY → laptop perfectly parallel to page + * -12deg: top of scene tilts toward viewer (viewer looks slightly up) + * + * Lid: transform-origin 50% 100% (hinge at bottom) + * closed: rotateX(-90deg) → flat over keyboard, screenshot hidden + * open: rotateX(0deg) → vertical, screen faces viewer frontally ✓ + * + * Keyboard: position:absolute at top:100% (just below lid = hinge) + * transform-origin: 50% 0% → pivot at its top edge = hinge + * transform: rotateX(90deg) → keyboard lies flat, horizontal, extending toward viewer + * height: 155px → becomes the keyboard's 3D depth + * Key surface (CSS front face) faces upward → visible from viewer below ✓ + * Perspective creates strong foreshortening: front edge close, hinge far ✓ + */ + + const lidTransform = isOpen ? 'rotateX(0deg)' : 'rotateX(-90deg)' + const lidTransition = isOpen + ? 'transform 2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s' + : 'none' + const screenOpacity = isOpen ? 1 : 0 + const screenTransition = isOpen ? 'opacity 0.8s ease-out 1.3s' : 'none' + + return ( +
+ {/* Ambient glow — appears with the screen */} +
+ + {/* Stage — rotateX(-12deg) only for perfect alignment with page elements */} +
+ {/* ── LID ── + pivot: 50% 100% = bottom edge = hinge + closed → open: -90deg → 0deg */} +
+
+ {/* Notch with camera */} +
+
+
+ + {/* Screen bezel */} +
+
+
+ {alt} +
+
+
+ +
+
+
+ + {/* ── KEYBOARD BASE ── + Positioned absolutely at the hinge (top: 100% of lid in DOM). + transform-origin: top → pivot at hinge. + rotateX(90deg) → keyboard lies FLAT and horizontal, + extending toward the viewer. + With stage rotateX(-12deg) (viewer below), the key surface (CSS front face, + now pointing upward) is visible from below → strong perspective effect. ✓ */} +
+ {/* Hinge groove at top of keyboard */} +
+ + {/* Keyboard rows */} +
+ {[13, 14, 13, 10].map((n, row) => ( +
+ {Array.from({ length: n }).map((_, i) => ( +
+ ))} +
+ ))} +
+ + {/* Trackpad */} +
+ + {/* Front thickness face — rotateX(-90deg) inside parent's rotateX(90deg) = vertical, facing viewer */} +
+
+
+ + {/* Ground shadow */} +
+
+ ) +} diff --git a/src/pages/products/Desktop.tsx b/src/pages/products/Desktop.tsx index 52fd738..50bbb6e 100644 --- a/src/pages/products/Desktop.tsx +++ b/src/pages/products/Desktop.tsx @@ -8,7 +8,7 @@ import { footerConfig } from '@/constants/footer' import { DOCS } from '@/constants/urls' import { useTranslation } from 'react-i18next' import { AnimateIn } from '@/components/animations/AnimateIn' -import { TiltCard } from '@/components/common/TiltCard' +import { MacbookMockup } from '@/components/common/MacbookMockup' import { motion } from 'framer-motion' const features = [ @@ -138,9 +138,9 @@ export const Desktop = () => {
- {/* Desktop App Screenshot */} - - +