feat: add brand logo and animated background
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,145 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
interface AnimatedBackgroundProps {
|
||||||
|
/** Intensity multiplier (0.5 = subtle, 1 = default, 1.5 = bold) */
|
||||||
|
intensity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnimatedBackground({ intensity = 1 }: AnimatedBackgroundProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
const base = isDark ? 0.25 : 0.4;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Orb
|
||||||
|
color={isDark ? 'rgba(255,107,0,0.6)' : 'rgba(255,107,0,0.55)'}
|
||||||
|
size={620}
|
||||||
|
opacity={base * intensity}
|
||||||
|
delay={0}
|
||||||
|
duration={28}
|
||||||
|
x={['-15%', '5%', '-10%']}
|
||||||
|
y={['-10%', '15%', '0%']}
|
||||||
|
scale={[1, 1.15, 1]}
|
||||||
|
/>
|
||||||
|
<Orb
|
||||||
|
color={isDark ? 'rgba(45,90,39,0.55)' : 'rgba(45,90,39,0.45)'}
|
||||||
|
size={520}
|
||||||
|
opacity={base * intensity * 0.9}
|
||||||
|
delay={4}
|
||||||
|
duration={34}
|
||||||
|
x={['70%', '50%', '75%']}
|
||||||
|
y={['10%', '30%', '5%']}
|
||||||
|
scale={[1, 1.1, 1]}
|
||||||
|
/>
|
||||||
|
<Orb
|
||||||
|
color={isDark ? 'rgba(59,130,246,0.35)' : 'rgba(59,130,246,0.3)'}
|
||||||
|
size={480}
|
||||||
|
opacity={base * intensity * 0.7}
|
||||||
|
delay={8}
|
||||||
|
duration={40}
|
||||||
|
x={['20%', '40%', '15%']}
|
||||||
|
y={['65%', '50%', '70%']}
|
||||||
|
scale={[1, 1.08, 1]}
|
||||||
|
/>
|
||||||
|
<Orb
|
||||||
|
color={isDark ? 'rgba(245,158,11,0.3)' : 'rgba(245,158,11,0.28)'}
|
||||||
|
size={420}
|
||||||
|
opacity={base * intensity * 0.6}
|
||||||
|
delay={2}
|
||||||
|
duration={36}
|
||||||
|
x={['60%', '80%', '55%']}
|
||||||
|
y={['70%', '55%', '80%']}
|
||||||
|
scale={[1, 1.12, 1]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Subtle grid pattern */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
opacity: isDark ? 0.18 : 0.35,
|
||||||
|
backgroundImage:
|
||||||
|
isDark
|
||||||
|
? 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.05) 1px, transparent 0)'
|
||||||
|
: 'radial-gradient(circle at 1px 1px, rgba(15,23,42,0.06) 1px, transparent 0)',
|
||||||
|
backgroundSize: '28px 28px',
|
||||||
|
maskImage:
|
||||||
|
'linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.85) 25%, rgba(0,0,0,0.85) 75%, transparent 100%)',
|
||||||
|
WebkitMaskImage:
|
||||||
|
'linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.85) 25%, rgba(0,0,0,0.85) 75%, transparent 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Vignette so foreground content reads clearly */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: isDark
|
||||||
|
? 'radial-gradient(circle at 50% 30%, transparent 0%, rgba(11,15,26,0.4) 80%)'
|
||||||
|
: 'radial-gradient(circle at 50% 30%, transparent 0%, rgba(249,250,251,0.45) 80%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrbProps {
|
||||||
|
color: string;
|
||||||
|
size: number;
|
||||||
|
opacity: number;
|
||||||
|
delay: number;
|
||||||
|
duration: number;
|
||||||
|
x: string[];
|
||||||
|
y: string[];
|
||||||
|
scale: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Orb({ color, size, opacity, delay, duration, x, y, scale }: OrbProps) {
|
||||||
|
// Build keyframes inline as a CSS custom animation
|
||||||
|
const animName = `pp-orb-${Math.abs(
|
||||||
|
delay * 31 + duration * 7 + size
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const keyframes = `
|
||||||
|
@keyframes ${animName} {
|
||||||
|
0% { transform: translate3d(${x[0]}, ${y[0]}, 0) scale(${scale[0]}); }
|
||||||
|
50% { transform: translate3d(${x[1]}, ${y[1]}, 0) scale(${scale[1]}); }
|
||||||
|
100% { transform: translate3d(${x[2]}, ${y[2]}, 0) scale(${scale[2]}); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{keyframes}</style>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: `radial-gradient(circle, ${color} 0%, transparent 65%)`,
|
||||||
|
filter: 'blur(80px)',
|
||||||
|
opacity,
|
||||||
|
willChange: 'transform',
|
||||||
|
animation: `${animName} ${duration}s ease-in-out ${delay}s infinite alternate`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
interface LogoProps {
|
||||||
|
/** Total box size in pixels. */
|
||||||
|
size?: number;
|
||||||
|
/** Show the rounded white container backplate (for use over coloured surfaces). */
|
||||||
|
withBackplate?: boolean;
|
||||||
|
/** Padding inside the backplate (if any). */
|
||||||
|
padding?: number;
|
||||||
|
/** Optional border-radius override (defaults to 2 = 8px). */
|
||||||
|
rounded?: number;
|
||||||
|
/** className for parent positioning. */
|
||||||
|
className?: string;
|
||||||
|
/** Click handler. */
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Logo({
|
||||||
|
size = 36,
|
||||||
|
withBackplate = false,
|
||||||
|
padding,
|
||||||
|
rounded,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: LogoProps) {
|
||||||
|
const inner = (
|
||||||
|
<Image
|
||||||
|
src="/logo.png"
|
||||||
|
alt="PatchPilot Logo"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
priority
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!withBackplate) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: onClick ? 'pointer' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
flexShrink: 0,
|
||||||
|
p: padding ?? 0.5,
|
||||||
|
borderRadius: rounded ?? 2,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'var(--pp-shadow-soft)',
|
||||||
|
cursor: onClick ? 'pointer' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user