/** * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates * SPDX-License-Identifier: MIT */ import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { useDark } from '@rspress/core/runtime'; import './index.css'; // Performance configuration based on device capabilities interface PerformanceConfig { enabled: boolean; meteorCount: number; maxFlameTrails: number; trailLength: number; animationQuality: 'high' | 'medium' | 'low'; frameSkip: number; } // Performance detection utilities const detectPerformance = (): PerformanceConfig => { // Check for reduced motion preference if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return { enabled: false, meteorCount: 0, maxFlameTrails: 0, trailLength: 0, animationQuality: 'low', frameSkip: 0, }; } // Basic device capability detection const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { return { enabled: false, meteorCount: 0, maxFlameTrails: 0, trailLength: 0, animationQuality: 'low', frameSkip: 0, }; } // Check hardware concurrency (CPU cores) const cores = navigator.hardwareConcurrency || 2; // Check memory (if available) const memory = (navigator as any).deviceMemory || 4; // Check if mobile device const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ); // Performance scoring let score = 0; score += cores >= 4 ? 2 : cores >= 2 ? 1 : 0; score += memory >= 8 ? 2 : memory >= 4 ? 1 : 0; score += isMobile ? -1 : 1; // Configure based on performance score if (score >= 4) { // High performance return { enabled: true, meteorCount: 12, maxFlameTrails: 15, trailLength: 20, animationQuality: 'high', frameSkip: 1, }; } else if (score >= 2) { // Medium performance return { enabled: true, meteorCount: 8, maxFlameTrails: 8, trailLength: 12, animationQuality: 'medium', frameSkip: 2, }; } else { // Low performance - disable return { enabled: false, meteorCount: 0, maxFlameTrails: 0, trailLength: 0, animationQuality: 'low', frameSkip: 0, }; } }; // Trail particle interface for flame effect interface TrailParticle { x: number; y: number; vx: number; vy: number; size: number; tick: number; life: number; alpha: number; color: string; } // Meteor particle interface definition interface MeteorParticle { x: number; y: number; vx: number; vy: number; radius: number; color: string; alpha: number; trail: Array<{ x: number; y: number; alpha: number }>; trailLength: number; speed: number; angle: number; // New flame trail system flameTrails: TrailParticle[]; maxFlameTrails: number; } // Configuration options for flame effects const flameOptions = { trailSizeBaseMultiplier: 0.6, trailSizeAddedMultiplier: 0.3, trailSizeSpeedMultiplier: 0.15, trailAddedBaseRadiant: -0.8, trailAddedAddedRadiant: 3, trailBaseLifeSpan: 25, trailAddedLifeSpan: 20, trailGenerationChance: 0.3, }; // Trail class for managing individual flame particles class Trail { private particle: TrailParticle; private parentMeteor: MeteorParticle; constructor(parent: MeteorParticle) { this.parentMeteor = parent; this.particle = this.createTrailParticle(); } // Create a new trail particle based on parent meteor private createTrailParticle = (): TrailParticle => { const baseSize = this.parentMeteor.radius * (flameOptions.trailSizeBaseMultiplier + flameOptions.trailSizeAddedMultiplier * Math.random()); const radiantOffset = flameOptions.trailAddedBaseRadiant + flameOptions.trailAddedAddedRadiant * Math.random(); const trailAngle = this.parentMeteor.angle + radiantOffset; const speed = baseSize * flameOptions.trailSizeSpeedMultiplier; return { x: this.parentMeteor.x + (Math.random() - 0.5) * this.parentMeteor.radius, y: this.parentMeteor.y + (Math.random() - 0.5) * this.parentMeteor.radius, vx: speed * Math.cos(trailAngle), vy: speed * Math.sin(trailAngle), size: baseSize, tick: 0, life: Math.floor( flameOptions.trailBaseLifeSpan + flameOptions.trailAddedLifeSpan * Math.random() ), alpha: 0.8 + Math.random() * 0.2, color: this.parentMeteor.color, }; }; // Update trail particle position and lifecycle public step = (): boolean => { this.particle.tick++; // Check if trail particle should be removed if (this.particle.tick > this.particle.life) { return false; // Signal for removal } // Update position this.particle.x += this.particle.vx; this.particle.y += this.particle.vy; // Apply slight deceleration for more realistic flame behavior this.particle.vx *= 0.98; this.particle.vy *= 0.98; return true; // Continue existing }; // Render the trail particle public draw = (ctx: CanvasRenderingContext2D): void => { const lifeRatio = 1 - this.particle.tick / this.particle.life; const currentSize = this.particle.size * lifeRatio; const currentAlpha = this.particle.alpha * lifeRatio; if (currentSize <= 0 || currentAlpha <= 0) return; const alphaHex = Math.floor(currentAlpha * 255) .toString(16) .padStart(2, '0'); // Draw flame particle with gradient const gradient = ctx.createRadialGradient( this.particle.x, this.particle.y, 0, this.particle.x, this.particle.y, currentSize * 2 ); gradient.addColorStop(0, this.particle.color + 'FF'); gradient.addColorStop(0.4, this.particle.color + alphaHex); gradient.addColorStop(0.8, this.particle.color + '33'); gradient.addColorStop(1, this.particle.color + '00'); ctx.beginPath(); ctx.arc(this.particle.x, this.particle.y, currentSize, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.fill(); // Add glow effect ctx.shadowColor = this.particle.color; ctx.shadowBlur = currentSize * 2; ctx.fill(); ctx.shadowBlur = 0; }; } // Background2 component - Circular meteor with enhanced flame trailing effect export const Background: React.FC = () => { const canvasRef = useRef(null); const animationRef = useRef(0); const meteorsRef = useRef([]); const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const frameCountRef = useRef(0); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const isDark = useDark(); // Performance configuration - memoized to avoid recalculation const performanceConfig = useMemo(() => detectPerformance(), []); // Early return if animation is disabled if (!performanceConfig.enabled) { return null; } // Color configuration - adjusted based on theme mode const lightColors = ['#4062A7', '#5482BE', '#5ABAC2', '#86C8C5']; const darkColors = ['#6B8CFF', '#8DA9FF', '#7FDBDA', '#A8EDEA']; const colors = isDark ? darkColors : lightColors; // Initialize meteor particles with flame trail system - optimized based on performance const initMeteors = useCallback((): void => { meteorsRef.current = []; for (let i = 0; i < performanceConfig.meteorCount; i++) { const angle = Math.PI * 0.25; // Fixed 45 degrees (down-right) const radius = Math.random() * 1.2 + 1.8; // Slightly larger meteors for better flame effect const speed = (radius - 1.8) * 3.0 + 2.0; // Adjusted speed range const trailLength = Math.floor( Math.random() * (performanceConfig.trailLength * 0.5) + performanceConfig.trailLength * 0.5 ); meteorsRef.current.push({ x: Math.random() * dimensions.width, y: Math.random() * dimensions.height, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, radius, color: colors[Math.floor(Math.random() * colors.length)], alpha: Math.random() * 0.3 + 0.7, trail: [], trailLength, speed, angle, // Initialize flame trail system with performance-based limits flameTrails: [], maxFlameTrails: Math.floor( radius * performanceConfig.maxFlameTrails * 0.5 + performanceConfig.maxFlameTrails * 0.3 ), }); } }, [performanceConfig, dimensions.width, dimensions.height, colors]); // Update meteor trail (original trail system) - optimized const updateTrail = useCallback( (meteor: MeteorParticle): void => { meteor.trail.unshift({ x: meteor.x, y: meteor.y, alpha: meteor.alpha, }); if (meteor.trail.length > meteor.trailLength) { meteor.trail.pop(); } // Only update alpha for visible trail points in high quality mode if (performanceConfig.animationQuality === 'high') { meteor.trail.forEach((point, index) => { point.alpha = meteor.alpha * (1 - index / meteor.trailLength); }); } }, [performanceConfig.animationQuality] ); // Update flame trails system - optimized const updateFlameTrails = useCallback( (meteor: MeteorParticle): void => { // Reduce flame trail generation based on performance config const generationChance = performanceConfig.animationQuality === 'high' ? flameOptions.trailGenerationChance : performanceConfig.animationQuality === 'medium' ? flameOptions.trailGenerationChance * 0.7 : flameOptions.trailGenerationChance * 0.4; // Generate new flame trail particles if (meteor.flameTrails.length < meteor.maxFlameTrails && Math.random() < generationChance) { const trail = new Trail(meteor); meteor.flameTrails.push(trail as any); // Type assertion for compatibility } // Update existing flame trails and remove expired ones meteor.flameTrails = meteor.flameTrails.filter((trail: any) => trail.step && trail.step()); }, [performanceConfig.animationQuality] ); // Draw meteor with enhanced flame trailing effect - optimized const drawMeteor = useCallback( (ctx: CanvasRenderingContext2D, meteor: MeteorParticle): void => { // Draw flame trails first (behind the meteor) - skip in low quality mode if (performanceConfig.animationQuality !== 'low') { meteor.flameTrails.forEach((trail: any) => { if (trail.draw) { trail.draw(ctx); } }); } // Draw original trail system with reduced complexity for lower quality const trailStep = performanceConfig.animationQuality === 'high' ? 1 : performanceConfig.animationQuality === 'medium' ? 2 : 3; for (let i = 0; i < meteor.trail.length; i += trailStep) { const point = meteor.trail[i]; const trailRadius = meteor.radius * (1 - i / meteor.trail.length) * 0.8; const alpha = performanceConfig.animationQuality === 'high' ? point.alpha : meteor.alpha * (1 - i / meteor.trail.length); const alphaHex = Math.floor(alpha * 255) .toString(16) .padStart(2, '0'); ctx.beginPath(); ctx.arc(point.x, point.y, trailRadius, 0, Math.PI * 2); ctx.fillStyle = meteor.color + alphaHex; ctx.fill(); // Reduce shadow effects for better performance if (performanceConfig.animationQuality === 'high') { ctx.shadowColor = meteor.color; ctx.shadowBlur = trailRadius * 2 + meteor.radius; ctx.fill(); ctx.shadowBlur = 0; } } // Draw main meteor body ctx.beginPath(); ctx.arc(meteor.x, meteor.y, meteor.radius, 0, Math.PI * 2); // Simplified gradient for lower quality modes if (performanceConfig.animationQuality === 'high') { const gradient = ctx.createRadialGradient( meteor.x, meteor.y, 0, meteor.x, meteor.y, meteor.radius * 2.5 ); gradient.addColorStop(0, meteor.color + 'FF'); gradient.addColorStop(0.5, meteor.color + 'DD'); gradient.addColorStop(0.8, meteor.color + '77'); gradient.addColorStop(1, meteor.color + '00'); ctx.fillStyle = gradient; } else { ctx.fillStyle = meteor.color + 'DD'; } ctx.fill(); // Add bright core ctx.beginPath(); ctx.arc(meteor.x, meteor.y, meteor.radius * 0.7, 0, Math.PI * 2); ctx.fillStyle = meteor.color + 'FF'; ctx.fill(); // Enhanced outer glow - only in high quality mode if (performanceConfig.animationQuality === 'high') { ctx.shadowColor = meteor.color; ctx.shadowBlur = meteor.radius * 5 + 3; ctx.fill(); ctx.shadowBlur = 0; } }, [performanceConfig.animationQuality] ); // Update meteor position and behavior const updateMeteor = (meteor: MeteorParticle): void => { // Mouse interaction - meteors are slightly attracted to mouse const dx = mouseRef.current.x - meteor.x; const dy = mouseRef.current.y - meteor.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 120) { const force = ((120 - distance) / 120) * 0.008; // Slightly reduced force for stability const angle = Math.atan2(dy, dx); meteor.vx += Math.cos(angle) * force; meteor.vy += Math.sin(angle) * force; } // Update both trail systems before moving updateTrail(meteor); updateFlameTrails(meteor); // Update position meteor.x += meteor.vx; meteor.y += meteor.vy; // Boundary wrapping with smooth transition if (meteor.x > dimensions.width + meteor.radius * 3) { meteor.x = -meteor.radius * 3; meteor.trail = []; // Clear trail when wrapping meteor.flameTrails = []; // Clear flame trails when wrapping } if (meteor.x < -meteor.radius * 3) { meteor.x = dimensions.width + meteor.radius * 3; meteor.trail = []; meteor.flameTrails = []; } if (meteor.y > dimensions.height + meteor.radius * 3) { meteor.y = -meteor.radius * 3; meteor.trail = []; meteor.flameTrails = []; } if (meteor.y < -meteor.radius * 3) { meteor.y = dimensions.height + meteor.radius * 3; meteor.trail = []; meteor.flameTrails = []; } // Maintain consistent direction - gently guide back to base direction const baseAngle = Math.PI * 0.25; // 45 degrees // Gradually adjust direction meteor.vx += Math.cos(baseAngle) * 0.003; meteor.vy += Math.sin(baseAngle) * 0.003; // Apply slight damping to prevent excessive speed meteor.vx *= 0.999; meteor.vy *= 0.999; // Maintain minimum speed to keep meteors moving const currentSpeed = Math.sqrt(meteor.vx * meteor.vx + meteor.vy * meteor.vy); if (currentSpeed < 0.5) { meteor.vx = Math.cos(baseAngle) * 0.8; meteor.vy = Math.sin(baseAngle) * 0.8; } // Update meteor angle for flame trail generation meteor.angle = Math.atan2(meteor.vy, meteor.vx); }; // Animation loop - optimized with frame skipping const animate = useCallback((): void => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Frame skipping for performance optimization frameCountRef.current++; if (frameCountRef.current % performanceConfig.frameSkip !== 0) { animationRef.current = requestAnimationFrame(animate); return; } // Clear canvas completely to avoid permanent trails ctx.clearRect(0, 0, dimensions.width, dimensions.height); // Add subtle background with very low opacity for better flame visibility // Skip background overlay in low quality mode if (performanceConfig.animationQuality !== 'low') { const bgColor = isDark ? 'rgba(13, 17, 23, 0.015)' : 'rgba(237, 243, 248, 0.015)'; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, dimensions.width, dimensions.height); } // Update and draw meteors meteorsRef.current.forEach((meteor) => { updateMeteor(meteor); drawMeteor(ctx, meteor); }); animationRef.current = requestAnimationFrame(animate); }, [performanceConfig, dimensions, isDark, updateMeteor, drawMeteor]); // Handle window resize - optimized useEffect(() => { const handleResize = (): void => { setDimensions({ width: window.innerWidth, height: window.innerHeight, }); }; handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Handle mouse movement - optimized with throttling useEffect(() => { let throttleTimer: NodeJS.Timeout | null = null; const handleMouseMove = (e: MouseEvent): void => { if (throttleTimer) return; throttleTimer = setTimeout( () => { mouseRef.current = { x: e.clientX, y: e.clientY }; throttleTimer = null; }, performanceConfig.animationQuality === 'high' ? 16 : performanceConfig.animationQuality === 'medium' ? 32 : 64 ); }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); if (throttleTimer) { clearTimeout(throttleTimer); } }; }, [performanceConfig.animationQuality]); // Initialize and start animation - optimized useEffect(() => { if (dimensions.width === 0 || dimensions.height === 0) return; initMeteors(); animate(); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [dimensions, isDark, initMeteors, animate]); return ( ); };