|
| 1 | +import React, { useRef, useEffect, useState } from 'react'; |
| 2 | +import clsx from 'clsx'; |
| 3 | +import styles from './reactLogo.module.css'; |
| 4 | +import { ReactLogoProps } from './reactLogo.types'; |
| 5 | + |
| 6 | +// Helper to get ellipse point by angle (radians) |
| 7 | +function getEllipsePoint(rx: number, ry: number, angle: number) { |
| 8 | + return { |
| 9 | + x: rx * Math.cos(angle), |
| 10 | + y: ry * Math.sin(angle), |
| 11 | + }; |
| 12 | +} |
| 13 | + |
| 14 | +// Helper to create a short arc path (SVG) from angle1 to angle2 |
| 15 | +function describeArc(rx: number, ry: number, angle1: number, angle2: number) { |
| 16 | + const p1 = getEllipsePoint(rx, ry, angle1); |
| 17 | + const p2 = getEllipsePoint(rx, ry, angle2); |
| 18 | + // Large arc flag is 0 because the arc is always < 180deg |
| 19 | + return `M${p1.x},${p1.y} A${rx},${ry} 0 0,1 ${p2.x},${p2.y}`; |
| 20 | +} |
| 21 | + |
| 22 | +// For each electron, define its rotation offset (in radians) |
| 23 | +const ELECTRON_OFFSETS = [0, (2 * Math.PI) / 3, (4 * Math.PI) / 3]; |
| 24 | +const ELLIPSE_ROTATIONS = [0, 60, 120]; |
| 25 | + |
| 26 | +export const AnimatedReactLogo: React.FC<ReactLogoProps> = ({ className }) => { |
| 27 | + // Animation state for all electrons |
| 28 | + const [angle, setAngle] = useState(0); // radians |
| 29 | + const requestRef = useRef<number | undefined>(undefined); |
| 30 | + const duration = 4; // seconds for full orbit |
| 31 | + const rx = 110; |
| 32 | + const ry = 42; |
| 33 | + const electronRadius = 8; |
| 34 | + const trailLength = Math.PI / 5; // short arc (about 36deg) |
| 35 | + |
| 36 | + useEffect(() => { |
| 37 | + let start: number | null = null; |
| 38 | + const animate = (timestamp: number) => { |
| 39 | + if (!start) start = timestamp; |
| 40 | + const elapsed = (timestamp - start) / 1000; // seconds |
| 41 | + // Orbit: angle from 0 to 2PI over duration |
| 42 | + const newAngle = ((elapsed % duration) / duration) * Math.PI * 2; |
| 43 | + setAngle(newAngle); |
| 44 | + requestRef.current = requestAnimationFrame(animate); |
| 45 | + }; |
| 46 | + requestRef.current = requestAnimationFrame(animate); |
| 47 | + return () => requestRef.current && cancelAnimationFrame(requestRef.current); |
| 48 | + }, []); |
| 49 | + |
| 50 | + // For each electron, calculate its position and trail |
| 51 | + const electrons = ELECTRON_OFFSETS.map((offset, i) => { |
| 52 | + // The ellipse is rotated, so we just rotate the group in SVG |
| 53 | + const electronAngle = angle + offset; |
| 54 | + const electronPos = getEllipsePoint(rx, ry, electronAngle); |
| 55 | + const trailStart = electronAngle - trailLength; |
| 56 | + const trailEnd = electronAngle; |
| 57 | + const trailPath = describeArc(rx, ry, trailStart, trailEnd); |
| 58 | + return { |
| 59 | + pos: electronPos, |
| 60 | + trailPath, |
| 61 | + rotation: ELLIPSE_ROTATIONS[i], |
| 62 | + }; |
| 63 | + }); |
| 64 | + |
| 65 | + return ( |
| 66 | + <div className={clsx(styles.container, className)}> |
| 67 | + <svg width='800' height='800' viewBox='-150 -150 300 300'> |
| 68 | + <defs> |
| 69 | + <filter id='glow' x='-30%' y='-30%' width='160%' height='160%'> |
| 70 | + <feGaussianBlur stdDeviation='4' result='coloredBlur' /> |
| 71 | + <feMerge> |
| 72 | + <feMergeNode in='coloredBlur' /> |
| 73 | + <feMergeNode in='SourceGraphic' /> |
| 74 | + </feMerge> |
| 75 | + </filter> |
| 76 | + {/* Subtle blur for trail */} |
| 77 | + <filter id='trail-blur' x='-20%' y='-20%' width='140%' height='140%'> |
| 78 | + <feGaussianBlur stdDeviation='2' /> |
| 79 | + </filter> |
| 80 | + {/* Gradient for trail fade-out (uses CSS variable) */} |
| 81 | + <linearGradient id='trail-gradient' x1='0%' y1='0%' x2='100%' y2='0%'> |
| 82 | + <stop offset='0%' stopColor='var(--trail-color)' stopOpacity='1' /> |
| 83 | + <stop offset='100%' stopColor='var(--trail-color)' stopOpacity='0' /> |
| 84 | + </linearGradient> |
| 85 | + {/* Electron bulb gradient: white center, electron color edge, transparent outer */} |
| 86 | + <radialGradient id='electron-bulb-gradient' cx='50%' cy='50%' r='50%'> |
| 87 | + <stop offset='0%' stopColor='#fff' stopOpacity='1' /> |
| 88 | + <stop offset='40%' stopColor='var(--electron-color)' stopOpacity='1' /> |
| 89 | + <stop offset='100%' stopColor='var(--electron-color)' stopOpacity='0' /> |
| 90 | + </radialGradient> |
| 91 | + {/* Nucleus bulb gradient: very bright white center, logo color edge, transparent outer */} |
| 92 | + <radialGradient id='nucleus-bulb-gradient' cx='50%' cy='50%' r='50%'> |
| 93 | + <stop offset='0%' stopColor='#fff' stopOpacity='1' /> |
| 94 | + <stop offset='30%' stopColor='#fff' stopOpacity='0.8' /> |
| 95 | + <stop offset='60%' stopColor='var(--logo-color)' stopOpacity='0.7' /> |
| 96 | + <stop offset='100%' stopColor='var(--logo-color)' stopOpacity='0' /> |
| 97 | + </radialGradient> |
| 98 | + </defs> |
| 99 | + <g> |
| 100 | + <animateTransform attributeName='transform' type='rotate' from='0 0 0' to='360 0 0' dur='25s' repeatCount='indefinite' /> |
| 101 | + |
| 102 | + {/* Static ellipses and glowing nucleus bulb */} |
| 103 | + <g className={styles.ellipses}> |
| 104 | + <ellipse cx='0' cy='0' rx={rx} ry={ry} /> |
| 105 | + <ellipse cx='0' cy='0' rx={rx} ry={ry} transform='rotate(60)' /> |
| 106 | + <ellipse cx='0' cy='0' rx={rx} ry={ry} transform='rotate(120)' /> |
| 107 | + {/* Glowing nucleus bulb */} |
| 108 | + <circle cx='0' cy='0' r='18' fill='url(#nucleus-bulb-gradient)' stroke='none' /> |
| 109 | + </g> |
| 110 | + |
| 111 | + {/* Animated subtle trails and electrons for all 3 */} |
| 112 | + {electrons.map((e, i) => ( |
| 113 | + <g key={i} transform={`rotate(${e.rotation})`}> |
| 114 | + <path className={styles.trail} d={e.trailPath} stroke='url(#trail-gradient)' /> |
| 115 | + <circle className={styles.electron} cx={e.pos.x} cy={e.pos.y} r={electronRadius} fill='url(#electron-bulb-gradient)' /> |
| 116 | + </g> |
| 117 | + ))} |
| 118 | + </g> |
| 119 | + </svg> |
| 120 | + </div> |
| 121 | + ); |
| 122 | +}; |
0 commit comments