From 3e7b2beca1136a42700a7e1aebfe4c0fb2861a00 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 15 Nov 2025 18:00:09 +0000 Subject: Initial commit --- frontend/src/components/HeartLogo.tsx | 270 ++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 frontend/src/components/HeartLogo.tsx (limited to 'frontend/src/components/HeartLogo.tsx') diff --git a/frontend/src/components/HeartLogo.tsx b/frontend/src/components/HeartLogo.tsx new file mode 100644 index 0000000..2d407f7 --- /dev/null +++ b/frontend/src/components/HeartLogo.tsx @@ -0,0 +1,270 @@ +import React, { useState, useEffect } from 'react' + +interface HeartLogoProps { + size?: 'small' | 'medium' | 'large' | 'header' | 'header-no-rays' + className?: string + onClick?: () => void +} + +export function HeartLogo({ size = 'small', className = '', onClick }: HeartLogoProps) { + const [animationOffset, setAnimationOffset] = useState(0) + const [isAnimating, setIsAnimating] = useState(false) + + // Click handler to toggle animation + const handleClick = () => { + setIsAnimating(!isAnimating) + if (onClick) { + onClick() + } + } + + // Animation for header beams + useEffect(() => { + if (size !== 'header-no-rays' || !isAnimating) return + + let animationId: number + let startTime = Date.now() + + const animate = () => { + const elapsed = Date.now() - startTime + const rotationSpeed = 0.02 // Slow rotation speed + const offset = (elapsed * rotationSpeed) % 360 + setAnimationOffset(offset) + animationId = requestAnimationFrame(animate) + } + + animationId = requestAnimationFrame(animate) + + return () => { + if (animationId) { + cancelAnimationFrame(animationId) + } + } + }, [size, isAnimating]) + if (size === 'header') { + // Header version with rays filling the entire header rectangle + return ( +
+
+
+
+ + {/* Header rays matching loading screen exactly */} + + {[...Array(16)].map((_, i) => { + const angle = (i * 22.5); + const rayWidth = 2; // Much thinner rays + + // Create triangular ray path - thin rays extending to header edges + const startAngle = angle - rayWidth; + const endAngle = angle + rayWidth; + + const innerRadius = 0; // Start from center (heart center) + const outerRadius = 200; // Extend far beyond to reach all header edges + + const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius; + const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius; + const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius; + const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius; + const x3 = Math.cos(startAngle * Math.PI / 180) * outerRadius; + const y3 = Math.sin(startAngle * Math.PI / 180) * outerRadius; + const x4 = Math.cos(endAngle * Math.PI / 180) * outerRadius; + const y4 = Math.sin(endAngle * Math.PI / 180) * outerRadius; + + return ( + + ); + })} + + +
+
+ + + +
+
+
+
+ ) + } + + if (size === 'header-no-rays') { + // Header version with small red beams - compact sunlight effect + return ( +
+
+
+ + {/* Red beams extending across entire header rectangle - optimized for wide screens */} + + {[...Array(32)].map((_, i) => { + const angle = (i * 11.25) + animationOffset; // 32 rays, 11.25 degrees apart, animated + const rayWidth = 2; // Much narrower rays to show background between them + + // Create triangular ray path + const startAngle = angle - rayWidth; + const endAngle = angle + rayWidth; + + const innerRadius = 0; // Start from heart center + + // Calculate intersection with rectangle bounds for wide header + const getIntersectionWithRect = (angleInDegrees: number) => { + const rad = angleInDegrees * Math.PI / 180; + const dx = Math.cos(rad); + const dy = Math.sin(rad); + + // Header rectangle bounds (wide format ~3:1 ratio) + const rectBoundsX = 200; // Full width + const rectBoundsY = 67; // Height to match header proportions + + // Calculate intersection with each edge + let t = Infinity; + + // Right edge (x = rectBoundsX) + if (dx > 0) { + t = Math.min(t, rectBoundsX / dx); + } + // Left edge (x = -rectBoundsX) + if (dx < 0) { + t = Math.min(t, -rectBoundsX / dx); + } + // Top edge (y = -rectBoundsY) + if (dy < 0) { + t = Math.min(t, -rectBoundsY / dy); + } + // Bottom edge (y = rectBoundsY) + if (dy > 0) { + t = Math.min(t, rectBoundsY / dy); + } + + return { x: dx * t, y: dy * t }; + }; + + const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius; + const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius; + const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius; + const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius; + + const edge1 = getIntersectionWithRect(startAngle); + const edge2 = getIntersectionWithRect(endAngle); + + return ( + + ); + })} + + +
+
+ + + +
+
+
+ ) + } + + // Original square version for other sizes + return ( +
+
+
+
+ + {/* Rising Sun flag style rays - triangular rays extending to rectangle edges */} + + {[...Array(16)].map((_, i) => { + const angle = (i * 22.5); + const rayWidth = 11.25; // Half the angle between rays for triangular shape + + // Create triangular ray path + const startAngle = angle - rayWidth; + const endAngle = angle + rayWidth; + + const innerRadius = 0; // Start from center (heart center) + + // Calculate intersection with rectangle bounds + // Rectangle aspect ratio 120:80 = 3:2, so use different bounds for x and y + const getIntersectionWithRect = (angleInDegrees: number) => { + const rad = angleInDegrees * Math.PI / 180; + const dx = Math.cos(rad); + const dy = Math.sin(rad); + + // Rectangle bounds matching the 120x80 aspect ratio (3:2) + const rectBoundsX = 100; // Full width + const rectBoundsY = 67; // 2/3 of width to maintain 3:2 ratio + + // Calculate intersection with each edge + let t = Infinity; + + // Right edge (x = rectBoundsX) + if (dx > 0) { + t = Math.min(t, rectBoundsX / dx); + } + // Left edge (x = -rectBoundsX) + if (dx < 0) { + t = Math.min(t, -rectBoundsX / dx); + } + // Top edge (y = -rectBoundsY) + if (dy < 0) { + t = Math.min(t, -rectBoundsY / dy); + } + // Bottom edge (y = rectBoundsY) + if (dy > 0) { + t = Math.min(t, rectBoundsY / dy); + } + + return { x: dx * t, y: dy * t }; + }; + + const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius; + const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius; + const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius; + const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius; + + const edge1 = getIntersectionWithRect(startAngle); + const edge2 = getIntersectionWithRect(endAngle); + + return ( + + ); + })} + + +
+
+ + + +
+
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3