index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
  6. import { useDark } from '@rspress/core/runtime';
  7. import './index.css';
  8. // Performance configuration based on device capabilities
  9. interface PerformanceConfig {
  10. enabled: boolean;
  11. meteorCount: number;
  12. maxFlameTrails: number;
  13. trailLength: number;
  14. animationQuality: 'high' | 'medium' | 'low';
  15. frameSkip: number;
  16. }
  17. // Performance detection utilities
  18. const detectPerformance = (): PerformanceConfig => {
  19. // Check for reduced motion preference
  20. if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  21. return {
  22. enabled: false,
  23. meteorCount: 0,
  24. maxFlameTrails: 0,
  25. trailLength: 0,
  26. animationQuality: 'low',
  27. frameSkip: 0,
  28. };
  29. }
  30. // Basic device capability detection
  31. const canvas = document.createElement('canvas');
  32. const ctx = canvas.getContext('2d');
  33. if (!ctx) {
  34. return {
  35. enabled: false,
  36. meteorCount: 0,
  37. maxFlameTrails: 0,
  38. trailLength: 0,
  39. animationQuality: 'low',
  40. frameSkip: 0,
  41. };
  42. }
  43. // Check hardware concurrency (CPU cores)
  44. const cores = navigator.hardwareConcurrency || 2;
  45. // Check memory (if available)
  46. const memory = (navigator as any).deviceMemory || 4;
  47. // Check if mobile device
  48. const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
  49. navigator.userAgent
  50. );
  51. // Performance scoring
  52. let score = 0;
  53. score += cores >= 4 ? 2 : cores >= 2 ? 1 : 0;
  54. score += memory >= 8 ? 2 : memory >= 4 ? 1 : 0;
  55. score += isMobile ? -1 : 1;
  56. // Configure based on performance score
  57. if (score >= 4) {
  58. // High performance
  59. return {
  60. enabled: true,
  61. meteorCount: 12,
  62. maxFlameTrails: 15,
  63. trailLength: 20,
  64. animationQuality: 'high',
  65. frameSkip: 1,
  66. };
  67. } else if (score >= 2) {
  68. // Medium performance
  69. return {
  70. enabled: true,
  71. meteorCount: 8,
  72. maxFlameTrails: 8,
  73. trailLength: 12,
  74. animationQuality: 'medium',
  75. frameSkip: 2,
  76. };
  77. } else {
  78. // Low performance - disable
  79. return {
  80. enabled: false,
  81. meteorCount: 0,
  82. maxFlameTrails: 0,
  83. trailLength: 0,
  84. animationQuality: 'low',
  85. frameSkip: 0,
  86. };
  87. }
  88. };
  89. // Trail particle interface for flame effect
  90. interface TrailParticle {
  91. x: number;
  92. y: number;
  93. vx: number;
  94. vy: number;
  95. size: number;
  96. tick: number;
  97. life: number;
  98. alpha: number;
  99. color: string;
  100. }
  101. // Meteor particle interface definition
  102. interface MeteorParticle {
  103. x: number;
  104. y: number;
  105. vx: number;
  106. vy: number;
  107. radius: number;
  108. color: string;
  109. alpha: number;
  110. trail: Array<{ x: number; y: number; alpha: number }>;
  111. trailLength: number;
  112. speed: number;
  113. angle: number;
  114. // New flame trail system
  115. flameTrails: TrailParticle[];
  116. maxFlameTrails: number;
  117. }
  118. // Configuration options for flame effects
  119. const flameOptions = {
  120. trailSizeBaseMultiplier: 0.6,
  121. trailSizeAddedMultiplier: 0.3,
  122. trailSizeSpeedMultiplier: 0.15,
  123. trailAddedBaseRadiant: -0.8,
  124. trailAddedAddedRadiant: 3,
  125. trailBaseLifeSpan: 25,
  126. trailAddedLifeSpan: 20,
  127. trailGenerationChance: 0.3,
  128. };
  129. // Trail class for managing individual flame particles
  130. class Trail {
  131. private particle: TrailParticle;
  132. private parentMeteor: MeteorParticle;
  133. constructor(parent: MeteorParticle) {
  134. this.parentMeteor = parent;
  135. this.particle = this.createTrailParticle();
  136. }
  137. // Create a new trail particle based on parent meteor
  138. private createTrailParticle = (): TrailParticle => {
  139. const baseSize =
  140. this.parentMeteor.radius *
  141. (flameOptions.trailSizeBaseMultiplier +
  142. flameOptions.trailSizeAddedMultiplier * Math.random());
  143. const radiantOffset =
  144. flameOptions.trailAddedBaseRadiant + flameOptions.trailAddedAddedRadiant * Math.random();
  145. const trailAngle = this.parentMeteor.angle + radiantOffset;
  146. const speed = baseSize * flameOptions.trailSizeSpeedMultiplier;
  147. return {
  148. x: this.parentMeteor.x + (Math.random() - 0.5) * this.parentMeteor.radius,
  149. y: this.parentMeteor.y + (Math.random() - 0.5) * this.parentMeteor.radius,
  150. vx: speed * Math.cos(trailAngle),
  151. vy: speed * Math.sin(trailAngle),
  152. size: baseSize,
  153. tick: 0,
  154. life: Math.floor(
  155. flameOptions.trailBaseLifeSpan + flameOptions.trailAddedLifeSpan * Math.random()
  156. ),
  157. alpha: 0.8 + Math.random() * 0.2,
  158. color: this.parentMeteor.color,
  159. };
  160. };
  161. // Update trail particle position and lifecycle
  162. public step = (): boolean => {
  163. this.particle.tick++;
  164. // Check if trail particle should be removed
  165. if (this.particle.tick > this.particle.life) {
  166. return false; // Signal for removal
  167. }
  168. // Update position
  169. this.particle.x += this.particle.vx;
  170. this.particle.y += this.particle.vy;
  171. // Apply slight deceleration for more realistic flame behavior
  172. this.particle.vx *= 0.98;
  173. this.particle.vy *= 0.98;
  174. return true; // Continue existing
  175. };
  176. // Render the trail particle
  177. public draw = (ctx: CanvasRenderingContext2D): void => {
  178. const lifeRatio = 1 - this.particle.tick / this.particle.life;
  179. const currentSize = this.particle.size * lifeRatio;
  180. const currentAlpha = this.particle.alpha * lifeRatio;
  181. if (currentSize <= 0 || currentAlpha <= 0) return;
  182. const alphaHex = Math.floor(currentAlpha * 255)
  183. .toString(16)
  184. .padStart(2, '0');
  185. // Draw flame particle with gradient
  186. const gradient = ctx.createRadialGradient(
  187. this.particle.x,
  188. this.particle.y,
  189. 0,
  190. this.particle.x,
  191. this.particle.y,
  192. currentSize * 2
  193. );
  194. gradient.addColorStop(0, this.particle.color + 'FF');
  195. gradient.addColorStop(0.4, this.particle.color + alphaHex);
  196. gradient.addColorStop(0.8, this.particle.color + '33');
  197. gradient.addColorStop(1, this.particle.color + '00');
  198. ctx.beginPath();
  199. ctx.arc(this.particle.x, this.particle.y, currentSize, 0, Math.PI * 2);
  200. ctx.fillStyle = gradient;
  201. ctx.fill();
  202. // Add glow effect
  203. ctx.shadowColor = this.particle.color;
  204. ctx.shadowBlur = currentSize * 2;
  205. ctx.fill();
  206. ctx.shadowBlur = 0;
  207. };
  208. }
  209. // Background2 component - Circular meteor with enhanced flame trailing effect
  210. export const Background: React.FC = () => {
  211. const canvasRef = useRef<HTMLCanvasElement>(null);
  212. const animationRef = useRef<number>(0);
  213. const meteorsRef = useRef<MeteorParticle[]>([]);
  214. const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
  215. const frameCountRef = useRef<number>(0);
  216. const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  217. const isDark = useDark();
  218. // Performance configuration - memoized to avoid recalculation
  219. const performanceConfig = useMemo(() => detectPerformance(), []);
  220. // Early return if animation is disabled
  221. if (!performanceConfig.enabled) {
  222. return null;
  223. }
  224. // Color configuration - adjusted based on theme mode
  225. const lightColors = ['#4062A7', '#5482BE', '#5ABAC2', '#86C8C5'];
  226. const darkColors = ['#6B8CFF', '#8DA9FF', '#7FDBDA', '#A8EDEA'];
  227. const colors = isDark ? darkColors : lightColors;
  228. // Initialize meteor particles with flame trail system - optimized based on performance
  229. const initMeteors = useCallback((): void => {
  230. meteorsRef.current = [];
  231. for (let i = 0; i < performanceConfig.meteorCount; i++) {
  232. const angle = Math.PI * 0.25; // Fixed 45 degrees (down-right)
  233. const radius = Math.random() * 1.2 + 1.8; // Slightly larger meteors for better flame effect
  234. const speed = (radius - 1.8) * 3.0 + 2.0; // Adjusted speed range
  235. const trailLength = Math.floor(
  236. Math.random() * (performanceConfig.trailLength * 0.5) + performanceConfig.trailLength * 0.5
  237. );
  238. meteorsRef.current.push({
  239. x: Math.random() * dimensions.width,
  240. y: Math.random() * dimensions.height,
  241. vx: Math.cos(angle) * speed,
  242. vy: Math.sin(angle) * speed,
  243. radius,
  244. color: colors[Math.floor(Math.random() * colors.length)],
  245. alpha: Math.random() * 0.3 + 0.7,
  246. trail: [],
  247. trailLength,
  248. speed,
  249. angle,
  250. // Initialize flame trail system with performance-based limits
  251. flameTrails: [],
  252. maxFlameTrails: Math.floor(
  253. radius * performanceConfig.maxFlameTrails * 0.5 + performanceConfig.maxFlameTrails * 0.3
  254. ),
  255. });
  256. }
  257. }, [performanceConfig, dimensions.width, dimensions.height, colors]);
  258. // Update meteor trail (original trail system) - optimized
  259. const updateTrail = useCallback(
  260. (meteor: MeteorParticle): void => {
  261. meteor.trail.unshift({
  262. x: meteor.x,
  263. y: meteor.y,
  264. alpha: meteor.alpha,
  265. });
  266. if (meteor.trail.length > meteor.trailLength) {
  267. meteor.trail.pop();
  268. }
  269. // Only update alpha for visible trail points in high quality mode
  270. if (performanceConfig.animationQuality === 'high') {
  271. meteor.trail.forEach((point, index) => {
  272. point.alpha = meteor.alpha * (1 - index / meteor.trailLength);
  273. });
  274. }
  275. },
  276. [performanceConfig.animationQuality]
  277. );
  278. // Update flame trails system - optimized
  279. const updateFlameTrails = useCallback(
  280. (meteor: MeteorParticle): void => {
  281. // Reduce flame trail generation based on performance config
  282. const generationChance =
  283. performanceConfig.animationQuality === 'high'
  284. ? flameOptions.trailGenerationChance
  285. : performanceConfig.animationQuality === 'medium'
  286. ? flameOptions.trailGenerationChance * 0.7
  287. : flameOptions.trailGenerationChance * 0.4;
  288. // Generate new flame trail particles
  289. if (meteor.flameTrails.length < meteor.maxFlameTrails && Math.random() < generationChance) {
  290. const trail = new Trail(meteor);
  291. meteor.flameTrails.push(trail as any); // Type assertion for compatibility
  292. }
  293. // Update existing flame trails and remove expired ones
  294. meteor.flameTrails = meteor.flameTrails.filter((trail: any) => trail.step && trail.step());
  295. },
  296. [performanceConfig.animationQuality]
  297. );
  298. // Draw meteor with enhanced flame trailing effect - optimized
  299. const drawMeteor = useCallback(
  300. (ctx: CanvasRenderingContext2D, meteor: MeteorParticle): void => {
  301. // Draw flame trails first (behind the meteor) - skip in low quality mode
  302. if (performanceConfig.animationQuality !== 'low') {
  303. meteor.flameTrails.forEach((trail: any) => {
  304. if (trail.draw) {
  305. trail.draw(ctx);
  306. }
  307. });
  308. }
  309. // Draw original trail system with reduced complexity for lower quality
  310. const trailStep =
  311. performanceConfig.animationQuality === 'high'
  312. ? 1
  313. : performanceConfig.animationQuality === 'medium'
  314. ? 2
  315. : 3;
  316. for (let i = 0; i < meteor.trail.length; i += trailStep) {
  317. const point = meteor.trail[i];
  318. const trailRadius = meteor.radius * (1 - i / meteor.trail.length) * 0.8;
  319. const alpha =
  320. performanceConfig.animationQuality === 'high'
  321. ? point.alpha
  322. : meteor.alpha * (1 - i / meteor.trail.length);
  323. const alphaHex = Math.floor(alpha * 255)
  324. .toString(16)
  325. .padStart(2, '0');
  326. ctx.beginPath();
  327. ctx.arc(point.x, point.y, trailRadius, 0, Math.PI * 2);
  328. ctx.fillStyle = meteor.color + alphaHex;
  329. ctx.fill();
  330. // Reduce shadow effects for better performance
  331. if (performanceConfig.animationQuality === 'high') {
  332. ctx.shadowColor = meteor.color;
  333. ctx.shadowBlur = trailRadius * 2 + meteor.radius;
  334. ctx.fill();
  335. ctx.shadowBlur = 0;
  336. }
  337. }
  338. // Draw main meteor body
  339. ctx.beginPath();
  340. ctx.arc(meteor.x, meteor.y, meteor.radius, 0, Math.PI * 2);
  341. // Simplified gradient for lower quality modes
  342. if (performanceConfig.animationQuality === 'high') {
  343. const gradient = ctx.createRadialGradient(
  344. meteor.x,
  345. meteor.y,
  346. 0,
  347. meteor.x,
  348. meteor.y,
  349. meteor.radius * 2.5
  350. );
  351. gradient.addColorStop(0, meteor.color + 'FF');
  352. gradient.addColorStop(0.5, meteor.color + 'DD');
  353. gradient.addColorStop(0.8, meteor.color + '77');
  354. gradient.addColorStop(1, meteor.color + '00');
  355. ctx.fillStyle = gradient;
  356. } else {
  357. ctx.fillStyle = meteor.color + 'DD';
  358. }
  359. ctx.fill();
  360. // Add bright core
  361. ctx.beginPath();
  362. ctx.arc(meteor.x, meteor.y, meteor.radius * 0.7, 0, Math.PI * 2);
  363. ctx.fillStyle = meteor.color + 'FF';
  364. ctx.fill();
  365. // Enhanced outer glow - only in high quality mode
  366. if (performanceConfig.animationQuality === 'high') {
  367. ctx.shadowColor = meteor.color;
  368. ctx.shadowBlur = meteor.radius * 5 + 3;
  369. ctx.fill();
  370. ctx.shadowBlur = 0;
  371. }
  372. },
  373. [performanceConfig.animationQuality]
  374. );
  375. // Update meteor position and behavior
  376. const updateMeteor = (meteor: MeteorParticle): void => {
  377. // Mouse interaction - meteors are slightly attracted to mouse
  378. const dx = mouseRef.current.x - meteor.x;
  379. const dy = mouseRef.current.y - meteor.y;
  380. const distance = Math.sqrt(dx * dx + dy * dy);
  381. if (distance < 120) {
  382. const force = ((120 - distance) / 120) * 0.008; // Slightly reduced force for stability
  383. const angle = Math.atan2(dy, dx);
  384. meteor.vx += Math.cos(angle) * force;
  385. meteor.vy += Math.sin(angle) * force;
  386. }
  387. // Update both trail systems before moving
  388. updateTrail(meteor);
  389. updateFlameTrails(meteor);
  390. // Update position
  391. meteor.x += meteor.vx;
  392. meteor.y += meteor.vy;
  393. // Boundary wrapping with smooth transition
  394. if (meteor.x > dimensions.width + meteor.radius * 3) {
  395. meteor.x = -meteor.radius * 3;
  396. meteor.trail = []; // Clear trail when wrapping
  397. meteor.flameTrails = []; // Clear flame trails when wrapping
  398. }
  399. if (meteor.x < -meteor.radius * 3) {
  400. meteor.x = dimensions.width + meteor.radius * 3;
  401. meteor.trail = [];
  402. meteor.flameTrails = [];
  403. }
  404. if (meteor.y > dimensions.height + meteor.radius * 3) {
  405. meteor.y = -meteor.radius * 3;
  406. meteor.trail = [];
  407. meteor.flameTrails = [];
  408. }
  409. if (meteor.y < -meteor.radius * 3) {
  410. meteor.y = dimensions.height + meteor.radius * 3;
  411. meteor.trail = [];
  412. meteor.flameTrails = [];
  413. }
  414. // Maintain consistent direction - gently guide back to base direction
  415. const baseAngle = Math.PI * 0.25; // 45 degrees
  416. // Gradually adjust direction
  417. meteor.vx += Math.cos(baseAngle) * 0.003;
  418. meteor.vy += Math.sin(baseAngle) * 0.003;
  419. // Apply slight damping to prevent excessive speed
  420. meteor.vx *= 0.999;
  421. meteor.vy *= 0.999;
  422. // Maintain minimum speed to keep meteors moving
  423. const currentSpeed = Math.sqrt(meteor.vx * meteor.vx + meteor.vy * meteor.vy);
  424. if (currentSpeed < 0.5) {
  425. meteor.vx = Math.cos(baseAngle) * 0.8;
  426. meteor.vy = Math.sin(baseAngle) * 0.8;
  427. }
  428. // Update meteor angle for flame trail generation
  429. meteor.angle = Math.atan2(meteor.vy, meteor.vx);
  430. };
  431. // Animation loop - optimized with frame skipping
  432. const animate = useCallback((): void => {
  433. const canvas = canvasRef.current;
  434. if (!canvas) return;
  435. const ctx = canvas.getContext('2d');
  436. if (!ctx) return;
  437. // Frame skipping for performance optimization
  438. frameCountRef.current++;
  439. if (frameCountRef.current % performanceConfig.frameSkip !== 0) {
  440. animationRef.current = requestAnimationFrame(animate);
  441. return;
  442. }
  443. // Clear canvas completely to avoid permanent trails
  444. ctx.clearRect(0, 0, dimensions.width, dimensions.height);
  445. // Add subtle background with very low opacity for better flame visibility
  446. // Skip background overlay in low quality mode
  447. if (performanceConfig.animationQuality !== 'low') {
  448. const bgColor = isDark ? 'rgba(13, 17, 23, 0.015)' : 'rgba(237, 243, 248, 0.015)';
  449. ctx.fillStyle = bgColor;
  450. ctx.fillRect(0, 0, dimensions.width, dimensions.height);
  451. }
  452. // Update and draw meteors
  453. meteorsRef.current.forEach((meteor) => {
  454. updateMeteor(meteor);
  455. drawMeteor(ctx, meteor);
  456. });
  457. animationRef.current = requestAnimationFrame(animate);
  458. }, [performanceConfig, dimensions, isDark, updateMeteor, drawMeteor]);
  459. // Handle window resize - optimized
  460. useEffect(() => {
  461. const handleResize = (): void => {
  462. setDimensions({
  463. width: window.innerWidth,
  464. height: window.innerHeight,
  465. });
  466. };
  467. handleResize();
  468. window.addEventListener('resize', handleResize);
  469. return () => window.removeEventListener('resize', handleResize);
  470. }, []);
  471. // Handle mouse movement - optimized with throttling
  472. useEffect(() => {
  473. let throttleTimer: NodeJS.Timeout | null = null;
  474. const handleMouseMove = (e: MouseEvent): void => {
  475. if (throttleTimer) return;
  476. throttleTimer = setTimeout(
  477. () => {
  478. mouseRef.current = { x: e.clientX, y: e.clientY };
  479. throttleTimer = null;
  480. },
  481. performanceConfig.animationQuality === 'high'
  482. ? 16
  483. : performanceConfig.animationQuality === 'medium'
  484. ? 32
  485. : 64
  486. );
  487. };
  488. window.addEventListener('mousemove', handleMouseMove);
  489. return () => {
  490. window.removeEventListener('mousemove', handleMouseMove);
  491. if (throttleTimer) {
  492. clearTimeout(throttleTimer);
  493. }
  494. };
  495. }, [performanceConfig.animationQuality]);
  496. // Initialize and start animation - optimized
  497. useEffect(() => {
  498. if (dimensions.width === 0 || dimensions.height === 0) return;
  499. initMeteors();
  500. animate();
  501. return () => {
  502. if (animationRef.current) {
  503. cancelAnimationFrame(animationRef.current);
  504. }
  505. };
  506. }, [dimensions, isDark, initMeteors, animate]);
  507. return (
  508. <canvas
  509. ref={canvasRef}
  510. width={dimensions.width}
  511. height={dimensions.height}
  512. className="background2-canvas"
  513. style={{
  514. position: 'absolute',
  515. width: '100%',
  516. height: '100%',
  517. pointerEvents: 'none',
  518. zIndex: -1,
  519. }}
  520. />
  521. );
  522. };