From a28a3a489d99752227d502c2fcaf8ddac5f82c0d Mon Sep 17 00:00:00 2001 From: Adriano Belisario Date: Mon, 4 May 2026 22:24:39 +0000 Subject: [PATCH] feat: add animated Mars planet decoration to header Co-Authored-By: Claude Sonnet 4.6 --- app/globals.css | 192 +++++++++++++++++++++++++++++++++++++ components/header.tsx | 3 + components/mars-planet.tsx | 43 +++++++++ 3 files changed, 238 insertions(+) create mode 100644 components/mars-planet.tsx diff --git a/app/globals.css b/app/globals.css index 01a53ca..cb1c411 100644 --- a/app/globals.css +++ b/app/globals.css @@ -75,6 +75,198 @@ body { opacity: 0.85; } +.mars-planet { + --mars-x: 0; + --mars-y: 0; + position: absolute; + z-index: 0; + width: clamp(5.5rem, 16vw, 8.5rem); + aspect-ratio: 1; + display: grid; + place-items: center; + cursor: pointer; + perspective: 600px; + filter: drop-shadow(0 24px 34px rgba(138, 47, 21, 0.24)); + transform: + translate3d(calc(var(--mars-x) * 6px), calc(var(--mars-y) * 6px), 0) + rotateX(calc(var(--mars-y) * -8deg)) + rotateY(calc(var(--mars-x) * 10deg)); + transition: transform 180ms ease, filter 180ms ease; +} + +.mars-planet:hover { + filter: drop-shadow(0 28px 38px rgba(138, 47, 21, 0.32)); +} + +.mars-planet:active { + transform: + translate3d(calc(var(--mars-x) * 5px), calc(var(--mars-y) * 5px), 0) + rotateX(calc(var(--mars-y) * -8deg)) + rotateY(calc(var(--mars-x) * 10deg)) + scale(0.96); +} + +.mars-planet__body, +.mars-planet__orbit, +.mars-planet__spark, +.mars-planet__band, +.mars-planet__crater { + position: absolute; + pointer-events: none; +} + +.mars-planet__body { + inset: 12%; + overflow: hidden; + border-radius: 9999px; + background: + radial-gradient(circle at 32% 28%, #ffc6a1 0 9%, transparent 10%), + radial-gradient(circle at 30% 32%, #e97445 0 34%, #c1502c 56%, #7c2814 100%); + box-shadow: + inset -18px -16px 30px rgba(71, 22, 12, 0.42), + inset 10px 9px 18px rgba(255, 219, 184, 0.34), + 0 0 34px rgba(231, 111, 65, 0.48); + animation: mars-float 5.5s ease-in-out infinite; +} + +.mars-planet__body::before { + content: ""; + position: absolute; + inset: -18% -24%; + background: + linear-gradient(16deg, transparent 20%, rgba(255, 196, 142, 0.24) 24% 29%, transparent 34%), + linear-gradient(-11deg, transparent 45%, rgba(121, 44, 24, 0.24) 49% 54%, transparent 59%); + animation: mars-drift 9s linear infinite; +} + +.mars-planet__orbit { + width: 116%; + height: 42%; + border: 2px solid rgba(255, 205, 165, 0.78); + border-left-color: transparent; + border-right-color: transparent; + border-radius: 9999px; + transform: rotate(-18deg); + box-shadow: 0 0 18px rgba(255, 188, 138, 0.34); + animation: mars-orbit 4.5s ease-in-out infinite; +} + +.mars-planet__band { + left: 8%; + right: 8%; + height: 11%; + border-radius: 9999px; + background: rgba(255, 178, 124, 0.26); + transform: rotate(-14deg); +} + +.mars-planet__band--one { + top: 36%; +} + +.mars-planet__band--two { + top: 58%; + background: rgba(113, 41, 23, 0.22); +} + +.mars-planet__crater { + border-radius: 9999px; + background: + radial-gradient(circle at 38% 35%, rgba(255, 219, 184, 0.24), transparent 36%), + rgba(106, 36, 19, 0.38); + box-shadow: inset 2px 2px 4px rgba(68, 21, 12, 0.34); +} + +.mars-planet__crater--one { + width: 16%; + height: 16%; + top: 30%; + left: 52%; +} + +.mars-planet__crater--two { + width: 11%; + height: 11%; + top: 58%; + left: 29%; +} + +.mars-planet__crater--three { + width: 9%; + height: 9%; + top: 64%; + left: 64%; +} + +.mars-planet__spark { + width: 0.48rem; + height: 0.48rem; + border-radius: 9999px; + background: #fff1df; + box-shadow: 0 0 12px rgba(255, 241, 223, 0.9); + animation: mars-spark 2.4s ease-in-out infinite; +} + +.mars-planet__spark--one { + top: 10%; + left: 17%; +} + +.mars-planet__spark--two { + right: 10%; + bottom: 18%; + width: 0.34rem; + height: 0.34rem; + animation-delay: 0.8s; +} + +@keyframes mars-float { + 0%, 100% { + transform: translateY(0) rotate(-2deg); + } + 50% { + transform: translateY(-8px) rotate(2deg); + } +} + +@keyframes mars-drift { + from { + transform: translateX(-10%); + } + to { + transform: translateX(10%); + } +} + +@keyframes mars-orbit { + 0%, 100% { + transform: rotate(-18deg) scaleX(1); + } + 50% { + transform: rotate(-18deg) scaleX(1.08); + } +} + +@keyframes mars-spark { + 0%, 100% { + opacity: 0.35; + transform: scale(0.72); + } + 50% { + opacity: 1; + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .mars-planet, + .mars-planet *, + .mars-planet *::before { + animation: none !important; + transition: none !important; + } +} + .bg-card { background-color: var(--card); } diff --git a/components/header.tsx b/components/header.tsx index 110069d..341f0fc 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,5 +1,7 @@ 'use client'; +import MarsPlanet from '@/components/mars-planet'; + interface HeaderProps { title: string; subtitle?: string; @@ -25,6 +27,7 @@ export default function Header({ title, subtitle, imageUrl, actions, maxWidth = />
+
{imageUrl && (
diff --git a/components/mars-planet.tsx b/components/mars-planet.tsx new file mode 100644 index 0000000..6e8e990 --- /dev/null +++ b/components/mars-planet.tsx @@ -0,0 +1,43 @@ +'use client'; + +import type { PointerEvent } from 'react'; + +type MarsPlanetProps = { + className?: string; +}; + +export default function MarsPlanet({ className = '' }: MarsPlanetProps) { + const handlePointerMove = (event: PointerEvent) => { + const bounds = event.currentTarget.getBoundingClientRect(); + const x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2; + const y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2; + + event.currentTarget.style.setProperty('--mars-x', x.toFixed(3)); + event.currentTarget.style.setProperty('--mars-y', y.toFixed(3)); + }; + + const resetTilt = (event: PointerEvent) => { + event.currentTarget.style.setProperty('--mars-x', '0'); + event.currentTarget.style.setProperty('--mars-y', '0'); + }; + + return ( + + ); +}