feat(a11y,ux): implement comprehensive accessibility and UX optimizations
Phase 1: Accessibility Optimizations - Add proper label associations and ARIA attributes to form inputs - Implement aria-required, aria-invalid, aria-describedby for better form accessibility - Add role='alert' for error messages - Enhance keyboard navigation with aria-expanded, aria-controls - Add aria-label for mobile menu button - Implement aria-current for active navigation items - Add semantic HTML with aria-labelledby for sections Phase 2: UX Optimizations - Create loading skeleton components for better loading states - Add FormSkeleton, SectionSkeleton, and LoadingSkeleton components - Prepare for lazy loading implementation Files modified: - src/components/ui/input.tsx: Enhanced with ARIA attributes - src/components/ui/textarea.tsx: Enhanced with ARIA attributes - src/components/layout/header.tsx: Added navigation ARIA labels - src/components/sections/hero-section.tsx: Added section labels - src/components/sections/services-section.tsx: Added section labels - src/components/ui/loading-skeleton.tsx: New loading state components Impact: - WCAG 2.1 AA compliance improvements - Better screen reader support - Enhanced keyboard navigation - Improved user feedback during loading
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface FluidWaveBackgroundProps {
|
||||
className?: string;
|
||||
color1?: string;
|
||||
color2?: string;
|
||||
speed?: number;
|
||||
intensity?: number;
|
||||
noiseScale?: number;
|
||||
mouseInfluence?: number;
|
||||
}
|
||||
|
||||
export function FluidWaveBackground({
|
||||
className = '',
|
||||
color1 = '#C41E3A',
|
||||
color2 = '#1C1C1C',
|
||||
speed = 0.5,
|
||||
intensity = 1.2,
|
||||
noiseScale = 3.0,
|
||||
mouseInfluence = 0.8
|
||||
}: FluidWaveBackgroundProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const meshRef = useRef<THREE.Mesh | null>(null);
|
||||
const animationRef = useRef<number>();
|
||||
const mouseRef = useRef({ x: 0, y: 0, active: false });
|
||||
|
||||
const vertexShader = `
|
||||
varying vec2 vUv;
|
||||
varying float vElevation;
|
||||
uniform float uTime;
|
||||
uniform float uIntensity;
|
||||
uniform float uNoiseScale;
|
||||
uniform vec2 uMouse;
|
||||
uniform float uMouseInfluence;
|
||||
uniform float uMouseActive;
|
||||
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
float fbm(vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
value += amplitude * noise(st);
|
||||
st *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vec2 pos = position.xy * uNoiseScale;
|
||||
float elevation = fbm(pos + uTime * 0.1);
|
||||
|
||||
if (uMouseActive > 0.5) {
|
||||
float dist = distance(uv, uMouse);
|
||||
float mouseEffect = smoothstep(0.3, 0.0, dist) * uMouseInfluence;
|
||||
elevation += mouseEffect * sin(uTime * 2.0 + dist * 10.0);
|
||||
}
|
||||
|
||||
vElevation = elevation * uIntensity;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, vElevation, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
varying vec2 vUv;
|
||||
varying float vElevation;
|
||||
uniform vec3 uColor1;
|
||||
uniform vec3 uColor2;
|
||||
uniform float uTime;
|
||||
|
||||
void main() {
|
||||
float mixFactor = smoothstep(-0.5, 0.5, vElevation);
|
||||
vec3 color = mix(uColor2, uColor1, mixFactor);
|
||||
|
||||
float highlight = smoothstep(0.3, 0.5, vElevation) * 0.3;
|
||||
color += vec3(highlight);
|
||||
|
||||
float alpha = 0.6 + vElevation * 0.2;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
sceneRef.current = scene;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||
camera.position.z = 5;
|
||||
cameraRef.current = camera;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(renderer.domElement);
|
||||
rendererRef.current = renderer;
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(10, 10, 128, 128);
|
||||
|
||||
const uniforms = {
|
||||
uTime: { value: 0 },
|
||||
uColor1: { value: new THREE.Color(color1) },
|
||||
uColor2: { value: new THREE.Color(color2) },
|
||||
uIntensity: { value: intensity },
|
||||
uNoiseScale: { value: noiseScale },
|
||||
uMouse: { value: new THREE.Vector2(0, 0) },
|
||||
uMouseInfluence: { value: mouseInfluence },
|
||||
uMouseActive: { value: 0 }
|
||||
};
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.rotation.x = -Math.PI / 4;
|
||||
scene.add(mesh);
|
||||
meshRef.current = mesh;
|
||||
|
||||
const animate = (time: number) => {
|
||||
if (meshRef.current && rendererRef.current && sceneRef.current && cameraRef.current) {
|
||||
const material = meshRef.current.material as THREE.ShaderMaterial;
|
||||
material.uniforms.uTime.value = time * speed;
|
||||
|
||||
if (mouseRef.current.active) {
|
||||
material.uniforms.uMouse.value.x = mouseRef.current.x;
|
||||
material.uniforms.uMouse.value.y = mouseRef.current.y;
|
||||
material.uniforms.uMouseActive.value = 1.0;
|
||||
} else {
|
||||
material.uniforms.uMouseActive.value = 0.0;
|
||||
}
|
||||
|
||||
rendererRef.current.render(sceneRef.current, cameraRef.current);
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
mouseRef.current.x = (event.clientX - rect.left) / rect.width;
|
||||
mouseRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height;
|
||||
mouseRef.current.active = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseRef.current.active = false;
|
||||
};
|
||||
|
||||
containerRef.current.addEventListener('mousemove', handleMouseMove);
|
||||
containerRef.current.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
animate(0);
|
||||
|
||||
const handleResize = () => {
|
||||
if (!containerRef.current || !cameraRef.current || !rendererRef.current) return;
|
||||
|
||||
const newWidth = containerRef.current.clientWidth;
|
||||
const newHeight = containerRef.current.clientHeight;
|
||||
|
||||
cameraRef.current.aspect = newWidth / newHeight;
|
||||
cameraRef.current.updateProjectionMatrix();
|
||||
rendererRef.current.setSize(newWidth, newHeight);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
containerRef.current?.removeEventListener('mousemove', handleMouseMove);
|
||||
containerRef.current?.removeEventListener('mouseleave', handleMouseLeave);
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
container.removeChild(rendererRef.current.domElement);
|
||||
}
|
||||
if (meshRef.current) {
|
||||
meshRef.current.geometry.dispose();
|
||||
(meshRef.current.material as THREE.ShaderMaterial).dispose();
|
||||
}
|
||||
};
|
||||
}, [color1, color2, speed, intensity, noiseScale, vertexShader, fragmentShader]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FluidWaveBackground;
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface GeometricShapeProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function GeometricShapes({
|
||||
className = '',
|
||||
color = '#C41E3A'
|
||||
}: GeometricShapeProps) {
|
||||
const shapes = [
|
||||
{ type: 'circle', size: 120, x: 10, y: 15, delay: 0 },
|
||||
{ type: 'square', size: 80, x: 80, y: 20, delay: 1 },
|
||||
{ type: 'triangle', size: 60, x: 70, y: 60, delay: 2 },
|
||||
{ type: 'circle', size: 40, x: 20, y: 70, delay: 3 },
|
||||
{ type: 'square', size: 50, x: 85, y: 75, delay: 4 }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{shapes.map((shape, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="absolute border-2"
|
||||
style={{
|
||||
borderColor: `${color}20`,
|
||||
width: shape.size,
|
||||
height: shape.size,
|
||||
left: `${shape.x}%`,
|
||||
top: `${shape.y}%`,
|
||||
borderRadius: shape.type === 'circle' ? '50%' : '0',
|
||||
transform: shape.type === 'triangle' ? 'rotate(0deg)' : 'rotate(0deg)'
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0, rotate: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.15, 0.15, 0],
|
||||
scale: [0, 1, 1, 0],
|
||||
rotate: [0, 45, 45, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
delay: shape.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.2, 0.8, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeometricShapes;
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface GlowEffectProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export function GlowEffect({
|
||||
className = '',
|
||||
color = '#C41E3A',
|
||||
count = 3
|
||||
}: GlowEffectProps) {
|
||||
const [glows, setGlows] = useState<Array<{
|
||||
id: number;
|
||||
size: number;
|
||||
x: number;
|
||||
y: number;
|
||||
delay: number;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generatedGlows = Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
size: 150 + Math.random() * 100,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
delay: i * 2
|
||||
}));
|
||||
setGlows(generatedGlows);
|
||||
}, [count]);
|
||||
|
||||
if (glows.length === 0) {
|
||||
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{glows.map((glow) => (
|
||||
<motion.div
|
||||
key={glow.id}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: glow.size,
|
||||
height: glow.size,
|
||||
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
||||
left: `${glow.x}%`,
|
||||
top: `${glow.y}%`,
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{
|
||||
opacity: [0, 0.4, 0.4, 0],
|
||||
scale: [0.5, 1.2, 1.2, 0.5]
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
delay: glow.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.3, 0.7, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlowEffect;
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface GradientAnimationProps {
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export function GradientAnimation({
|
||||
className = '',
|
||||
colors = ['#C41E3A', '#1C1C1C', '#D4A574'],
|
||||
duration = 8
|
||||
}: GradientAnimationProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`absolute inset-0 ${className}`}
|
||||
animate={{
|
||||
background: colors.map((color, i) =>
|
||||
`${color} ${100 / colors.length * i}% ${100 / colors.length * (i + 1)}%`
|
||||
).join(', ')
|
||||
}}
|
||||
transition={{
|
||||
duration,
|
||||
repeat: Infinity,
|
||||
ease: 'linear'
|
||||
}}
|
||||
style={{
|
||||
backgroundSize: '400% 400%',
|
||||
backgroundPosition: '0% 50%'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradientAnimation;
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface GradientFlowProps {
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export function GradientFlow({
|
||||
className = '',
|
||||
colors = ['#C41E3A', '#D4A574', '#8B4513', '#2F4F4F'],
|
||||
duration = 15
|
||||
}: GradientFlowProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`absolute inset-0 ${className}`}
|
||||
style={{
|
||||
background: `linear-gradient(-45deg, ${colors.join(', ')})`,
|
||||
backgroundSize: '400% 400%'
|
||||
}}
|
||||
animate={{
|
||||
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%']
|
||||
}}
|
||||
transition={{
|
||||
duration,
|
||||
repeat: Infinity,
|
||||
ease: 'linear'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradientFlow;
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface GradientGridProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
gridSize?: number;
|
||||
}
|
||||
|
||||
export function GradientGrid({
|
||||
className = '',
|
||||
color = '#C41E3A',
|
||||
gridSize = 8
|
||||
}: GradientGridProps) {
|
||||
const cells = Array.from({ length: gridSize }, (_, row) =>
|
||||
Array.from({ length: gridSize }, (_, col) => ({
|
||||
row,
|
||||
col,
|
||||
delay: (row + col) * 0.1
|
||||
}))
|
||||
).flat();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${gridSize}, 1fr)`,
|
||||
gap: '1px'
|
||||
}}
|
||||
>
|
||||
{cells.map((cell, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${color}05 0%, ${color}10 100%)`
|
||||
}}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.3, 0.3, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
delay: cell.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.3, 0.7, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradientGrid;
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface GridLinesProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
density?: number;
|
||||
}
|
||||
|
||||
export function GridLines({
|
||||
className = '',
|
||||
color = '#C41E3A',
|
||||
density = 6
|
||||
}: GridLinesProps) {
|
||||
const [lines, setLines] = useState<Array<{
|
||||
id: number;
|
||||
delay: number;
|
||||
duration: number;
|
||||
top: number;
|
||||
width: number;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generatedLines = Array.from({ length: density }, (_, i) => ({
|
||||
id: i,
|
||||
delay: i * 0.5,
|
||||
duration: 6 + Math.random() * 4,
|
||||
top: 20 + Math.random() * 60,
|
||||
width: 30 + Math.random() * 40
|
||||
}));
|
||||
setLines(generatedLines);
|
||||
}, [density]);
|
||||
|
||||
if (lines.length === 0) {
|
||||
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{lines.map((line) => (
|
||||
<motion.div
|
||||
key={line.id}
|
||||
className="absolute h-px"
|
||||
style={{
|
||||
backgroundColor: `${color}10`,
|
||||
left: `${(line.id / density) * 100}%`,
|
||||
top: `${line.top}%`,
|
||||
width: `${line.width}%`
|
||||
}}
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.2, 0.2, 0],
|
||||
scaleX: [0, 1, 1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: line.duration,
|
||||
delay: line.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.3, 0.7, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridLines;
|
||||
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
interface GSAPAnimationProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function GSAPAnimation({
|
||||
className = '',
|
||||
color = '#C41E3A'
|
||||
}: GSAPAnimationProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const elementsRef = useRef<HTMLDivElement[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const elements = elementsRef.current;
|
||||
|
||||
elements.forEach((el, i) => {
|
||||
gsap.fromTo(el,
|
||||
{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
rotation: 0
|
||||
},
|
||||
{
|
||||
opacity: 0.15,
|
||||
scale: 1,
|
||||
rotation: 360,
|
||||
duration: 8,
|
||||
delay: i * 1.5,
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
ease: 'power2.inOut'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
gsap.killTweensOf(elements);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const shapes = [
|
||||
{ type: 'circle', size: 100, x: 15, y: 20 },
|
||||
{ type: 'square', size: 80, x: 75, y: 15 },
|
||||
{ type: 'circle', size: 60, x: 65, y: 65 },
|
||||
{ type: 'square', size: 50, x: 20, y: 70 }
|
||||
];
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{shapes.map((shape, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={el => {
|
||||
if (el) elementsRef.current[index] = el;
|
||||
}}
|
||||
className="absolute border-2"
|
||||
style={{
|
||||
borderColor: `${color}20`,
|
||||
width: shape.size,
|
||||
height: shape.size,
|
||||
left: `${shape.x}%`,
|
||||
top: `${shape.y}%`,
|
||||
borderRadius: shape.type === 'circle' ? '50%' : '0'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GSAPAnimation;
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface MeshGradientProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MeshGradient({
|
||||
className = ''
|
||||
}: MeshGradientProps) {
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden ${className}`}>
|
||||
<motion.div
|
||||
className="absolute w-[800px] h-[800px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(196,30,58,0.15) 0%, transparent 70%)',
|
||||
filter: 'blur(60px)'
|
||||
}}
|
||||
animate={{
|
||||
x: [0, 100, 50, 0],
|
||||
y: [0, 50, 100, 0],
|
||||
scale: [1, 1.2, 0.9, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 20,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut'
|
||||
}}
|
||||
initial={{ left: '10%', top: '20%' }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute w-[600px] h-[600px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(212,165,116,0.12) 0%, transparent 70%)',
|
||||
filter: 'blur(50px)'
|
||||
}}
|
||||
animate={{
|
||||
x: [0, -80, -40, 0],
|
||||
y: [0, 80, 40, 0],
|
||||
scale: [1, 0.9, 1.1, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 18,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
delay: 2
|
||||
}}
|
||||
initial={{ right: '15%', top: '30%' }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute w-[500px] h-[500px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(139,69,19,0.1) 0%, transparent 70%)',
|
||||
filter: 'blur(40px)'
|
||||
}}
|
||||
animate={{
|
||||
x: [0, 60, -30, 0],
|
||||
y: [0, -60, 30, 0],
|
||||
scale: [1, 1.15, 0.95, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 22,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
delay: 4
|
||||
}}
|
||||
initial={{ left: '30%', bottom: '20%' }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute w-[400px] h-[400px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(196,30,58,0.08) 0%, transparent 70%)',
|
||||
filter: 'blur(30px)'
|
||||
}}
|
||||
animate={{
|
||||
x: [0, -50, 80, 0],
|
||||
y: [0, 40, -50, 0],
|
||||
scale: [1, 1.1, 0.85, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 16,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
delay: 1
|
||||
}}
|
||||
initial={{ right: '25%', bottom: '30%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeshGradient;
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ParallaxEffectProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
sensitivity?: number;
|
||||
}
|
||||
|
||||
export function ParallaxEffect({
|
||||
className = '',
|
||||
color = '#C41E3A',
|
||||
sensitivity = 0.05
|
||||
}: ParallaxEffectProps) {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
|
||||
const x = (e.clientX - rect.left - centerX) * sensitivity;
|
||||
const y = (e.clientY - rect.top - centerY) * sensitivity;
|
||||
|
||||
setMousePosition({ x, y });
|
||||
};
|
||||
|
||||
const container = containerRef.current;
|
||||
container?.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
container?.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, [sensitivity]);
|
||||
|
||||
const layers = [
|
||||
{ size: 300, x: 10, y: 15, factor: 1 },
|
||||
{ size: 200, x: 70, y: 20, factor: 1.5 },
|
||||
{ size: 150, x: 60, y: 60, factor: 2 },
|
||||
{ size: 100, x: 15, y: 65, factor: 2.5 }
|
||||
];
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{layers.map((layer, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: layer.size,
|
||||
height: layer.size,
|
||||
background: `radial-gradient(circle, ${color}15 0%, transparent 70%)`,
|
||||
left: `${layer.x}%`,
|
||||
top: `${layer.y}%`
|
||||
}}
|
||||
animate={{
|
||||
x: mousePosition.x * layer.factor,
|
||||
y: mousePosition.y * layer.factor
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 50,
|
||||
damping: 30
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParallaxEffect;
|
||||
@@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
interface ParticleGalaxyProps {
|
||||
particleCount?: number;
|
||||
connectionDistance?: number;
|
||||
mouseRadius?: number;
|
||||
particleColor?: string;
|
||||
lineColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ParticleGalaxy({
|
||||
particleCount = 100,
|
||||
connectionDistance = 150,
|
||||
mouseRadius = 150,
|
||||
particleColor = '196, 30, 58',
|
||||
lineColor = '196, 30, 58',
|
||||
className = ''
|
||||
}: ParticleGalaxyProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const animationRef = useRef<number>();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
const mouseInCanvas = useMotionValue(false);
|
||||
|
||||
const createParticle = useCallback((width: number, height: number): Particle => {
|
||||
return {
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
vx: (Math.random() - 0.5) * 0.8,
|
||||
vy: (Math.random() - 0.5) * 0.8,
|
||||
size: Math.random() * 2 + 1,
|
||||
opacity: Math.random() * 0.5 + 0.2
|
||||
};
|
||||
}, []);
|
||||
|
||||
const initParticles = useCallback((width: number, height: number) => {
|
||||
particlesRef.current = Array.from({ length: particleCount }, () =>
|
||||
createParticle(width, height)
|
||||
);
|
||||
}, [particleCount, createParticle]);
|
||||
|
||||
const drawParticles = useCallback((ctx: CanvasRenderingContext2D, width: number, height: number) => {
|
||||
const mx = mouseX.get();
|
||||
const my = mouseY.get();
|
||||
const inCanvas = mouseInCanvas.get();
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const particles = particlesRef.current;
|
||||
|
||||
particles.forEach((particle, i) => {
|
||||
let { x, y, vx, vy, size, opacity } = particle;
|
||||
|
||||
if (inCanvas) {
|
||||
const dx = x - mx;
|
||||
const dy = y - my;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < mouseRadius) {
|
||||
const force = (mouseRadius - distance) / mouseRadius;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
vx += Math.cos(angle) * force * 0.5;
|
||||
vy += Math.sin(angle) * force * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
x += vx;
|
||||
y += vy;
|
||||
|
||||
if (x < 0 || x > width) vx *= -1;
|
||||
if (y < 0 || y > height) vy *= -1;
|
||||
|
||||
x = Math.max(0, Math.min(width, x));
|
||||
y = Math.max(0, Math.min(height, y));
|
||||
|
||||
vx *= 0.99;
|
||||
vy *= 0.99;
|
||||
|
||||
const speed = Math.sqrt(vx * vx + vy * vy);
|
||||
if (speed < 0.1) {
|
||||
vx += (Math.random() - 0.5) * 0.1;
|
||||
vy += (Math.random() - 0.5) * 0.1;
|
||||
}
|
||||
|
||||
particlesRef.current[i] = { x, y, vx, vy, size, opacity };
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${particleColor}, ${opacity})`;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < connectionDistance) {
|
||||
const opacity = (1 - distance / connectionDistance) * 0.3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.strokeStyle = `rgba(${lineColor}, ${opacity})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [mouseX, mouseY, mouseInCanvas, mouseRadius, particleColor, lineColor, connectionDistance]);
|
||||
|
||||
const animate = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
drawParticles(ctx, canvas.width, canvas.height);
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
}, [drawParticles]);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const container = canvas.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
canvas.width = container.clientWidth;
|
||||
canvas.height = container.clientHeight;
|
||||
|
||||
initParticles(canvas.width, canvas.height);
|
||||
}, [initParticles]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const container = canvas.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
canvas.width = container.clientWidth;
|
||||
canvas.height = container.clientHeight;
|
||||
|
||||
initParticles(canvas.width, canvas.height);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
setIsVisible(entry.isIntersecting);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(canvas);
|
||||
|
||||
if (isVisible) {
|
||||
animate();
|
||||
}
|
||||
|
||||
const handleResizeWithDebounce = () => {
|
||||
setTimeout(handleResize, 250);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResizeWithDebounce);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', handleResizeWithDebounce);
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [isVisible, animate, initParticles, handleResize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
animate();
|
||||
} else if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
}, [isVisible, animate]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full"
|
||||
onMouseMove={(e) => {
|
||||
mouseX.set(e.clientX);
|
||||
mouseY.set(e.clientY);
|
||||
mouseInCanvas.set(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
mouseInCanvas.set(false);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParticleGalaxy;
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface SubtleDotsProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export function SubtleDots({
|
||||
className = '',
|
||||
color = '#C41E3A',
|
||||
count = 12
|
||||
}: SubtleDotsProps) {
|
||||
const [dots, setDots] = useState<Array<{
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
delay: number;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generatedDots = Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
x: 10 + Math.random() * 80,
|
||||
y: 10 + Math.random() * 80,
|
||||
size: 2 + Math.random() * 3,
|
||||
delay: i * 0.3
|
||||
}));
|
||||
setDots(generatedDots);
|
||||
}, [count]);
|
||||
|
||||
if (dots.length === 0) {
|
||||
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{dots.map((dot) => (
|
||||
<motion.div
|
||||
key={dot.id}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: dot.size,
|
||||
height: dot.size,
|
||||
backgroundColor: color,
|
||||
left: `${dot.x}%`,
|
||||
top: `${dot.y}%`
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.2, 0.2, 0],
|
||||
scale: [0, 1, 1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
delay: dot.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.3, 0.7, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubtleDots;
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface SubtleParticleProps {
|
||||
count?: number;
|
||||
size?: number;
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SubtleParticles({
|
||||
count = 20,
|
||||
size = 3,
|
||||
color = '#C41E3A',
|
||||
className = ''
|
||||
}: SubtleParticleProps) {
|
||||
const [particles, setParticles] = useState<Array<{
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
delay: number;
|
||||
duration: number;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generatedParticles = Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
delay: Math.random() * 5,
|
||||
duration: 8 + Math.random() * 4
|
||||
}));
|
||||
setParticles(generatedParticles);
|
||||
}, [count]);
|
||||
|
||||
if (particles.length === 0) {
|
||||
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
||||
{particles.map((particle) => (
|
||||
<motion.div
|
||||
key={particle.id}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: color,
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0.3, 0.3, 0],
|
||||
scale: [0, 1, 1, 0],
|
||||
y: [0, -20, -20, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: particle.duration,
|
||||
delay: particle.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
times: [0, 0.3, 0.7, 1]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubtleParticles;
|
||||
Reference in New Issue
Block a user