astro-component-kit 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +133 -36
  2. package/package.json +17 -4
  3. package/src/components/index.ts +18 -202
  4. package/src/components/lib/Galaxy3D.astro +249 -137
  5. package/src/components/lib/app/AppSidebar.astro +249 -0
  6. package/src/components/lib/app/AppTopbar.astro +284 -0
  7. package/src/components/lib/app/ArrivalsBoard.astro +176 -0
  8. package/src/components/lib/app/AuthCard.astro +213 -0
  9. package/src/components/lib/app/DataTable.astro +346 -0
  10. package/src/components/lib/app/GuestProfileCard.astro +133 -0
  11. package/src/components/lib/app/HousekeepingBoard.astro +189 -0
  12. package/src/components/lib/app/PageHeader.astro +157 -0
  13. package/src/components/lib/app/PlanUsageCard.astro +112 -0
  14. package/src/components/lib/app/RoomRack.astro +207 -0
  15. package/src/components/lib/app/SettingsSection.astro +96 -0
  16. package/src/components/lib/app/SwitchRow.astro +71 -0
  17. package/src/components/lib/app/TeamMembers.astro +190 -0
  18. package/src/components/lib/app/app-icons.ts +52 -0
  19. package/src/components/lib/app/index.ts +19 -0
  20. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  21. package/src/components/lib/buttons/RippleButton.astro +199 -0
  22. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  23. package/src/components/lib/buttons/index.ts +4 -0
  24. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  25. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  26. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  27. package/src/components/lib/cards/index.ts +3 -0
  28. package/src/components/lib/charts/ActivityRings.astro +105 -0
  29. package/src/components/lib/charts/AreaChart.astro +4 -4
  30. package/src/components/lib/charts/BarChart.astro +3 -3
  31. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  32. package/src/components/lib/charts/BubbleChart.astro +101 -0
  33. package/src/components/lib/charts/BulletChart.astro +160 -0
  34. package/src/components/lib/charts/BumpChart.astro +92 -0
  35. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  36. package/src/components/lib/charts/ChartFrame.astro +373 -0
  37. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  38. package/src/components/lib/charts/DonutChart.astro +2 -2
  39. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  40. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  41. package/src/components/lib/charts/FunnelChart.astro +122 -0
  42. package/src/components/lib/charts/GaugeChart.astro +111 -0
  43. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  44. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  45. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  46. package/src/components/lib/charts/HistogramChart.astro +111 -0
  47. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  48. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  49. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  50. package/src/components/lib/charts/LineChart.astro +160 -0
  51. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  52. package/src/components/lib/charts/LollipopChart.astro +100 -0
  53. package/src/components/lib/charts/MeterGauge.astro +122 -0
  54. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  55. package/src/components/lib/charts/PieChart.astro +135 -0
  56. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  57. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  58. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  59. package/src/components/lib/charts/RadarChart.astro +94 -0
  60. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  61. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  62. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  63. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  64. package/src/components/lib/charts/SankeyChart.astro +128 -0
  65. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  66. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  67. package/src/components/lib/charts/SlopeChart.astro +105 -0
  68. package/src/components/lib/charts/SparklineChart.astro +87 -0
  69. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  70. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  71. package/src/components/lib/charts/SunburstChart.astro +103 -0
  72. package/src/components/lib/charts/TreemapChart.astro +94 -0
  73. package/src/components/lib/charts/WaffleChart.astro +121 -0
  74. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  75. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  76. package/src/components/lib/charts/chart-utils.ts +271 -0
  77. package/src/components/lib/charts/index.ts +46 -0
  78. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  79. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  80. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  81. package/src/components/lib/data/index.ts +3 -0
  82. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  83. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  84. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  85. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  86. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  87. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  88. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  89. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  90. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  91. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  92. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  93. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  94. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  95. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  96. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  97. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  98. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  99. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  100. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  101. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  102. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  103. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  104. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  105. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  106. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  107. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  108. package/src/components/lib/ecommerce/index.ts +34 -0
  109. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  110. package/src/components/lib/ecommerce/types.ts +70 -0
  111. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  112. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  113. package/src/components/lib/feedback/index.ts +2 -0
  114. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  115. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  116. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  117. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  118. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  119. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  120. package/src/components/lib/forms/index.ts +5 -0
  121. package/src/components/lib/index.ts +2 -0
  122. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  123. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  124. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  125. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  126. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  127. package/src/components/lib/layout/FeatureHero.astro +4 -2
  128. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  129. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  130. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  131. package/src/components/lib/layout/HolyGrailGrid.astro +127 -0
  132. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  133. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  134. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  135. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  136. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  137. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  138. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  139. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  140. package/src/components/lib/layout/index.ts +3 -1
  141. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  142. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  143. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  144. package/src/components/lib/navigation/index.ts +3 -0
  145. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  146. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  147. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  148. package/src/components/lib/overlays/index.ts +3 -0
  149. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  150. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  151. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  152. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  153. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  154. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  155. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  156. package/src/components/lib/sections/BlogGridSection.astro +133 -0
  157. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  158. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  159. package/src/components/lib/sections/CareersOpenings.astro +141 -0
  160. package/src/components/lib/sections/ChangelogSection.astro +151 -0
  161. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  162. package/src/components/lib/sections/ContactSplitMap.astro +141 -0
  163. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  164. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  165. package/src/components/lib/sections/CtaNewsletterSection.astro +165 -0
  166. package/src/components/lib/sections/CtaSplitImage.astro +149 -0
  167. package/src/components/lib/sections/DataShowcase.astro +23 -21
  168. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  169. package/src/components/lib/sections/FaqTwoColumn.astro +121 -0
  170. package/src/components/lib/sections/FeatureAlternatingRows.astro +192 -0
  171. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  172. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  173. package/src/components/lib/sections/FeatureTabsShowcase.astro +182 -0
  174. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  175. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  176. package/src/components/lib/sections/FooterMegaSection.astro +157 -0
  177. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  178. package/src/components/lib/sections/HeroDashboardPreview.astro +182 -0
  179. package/src/components/lib/sections/HeroGlass.astro +22 -20
  180. package/src/components/lib/sections/HeroGradient.astro +15 -13
  181. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  182. package/src/components/lib/sections/HeroShowcaseSplit.astro +306 -0
  183. package/src/components/lib/sections/HeroSplit.astro +13 -11
  184. package/src/components/lib/sections/HeroTerminal.astro +253 -0
  185. package/src/components/lib/sections/HeroWaitlist.astro +231 -0
  186. package/src/components/lib/sections/HowItWorksSteps.astro +180 -0
  187. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  188. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  189. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  190. package/src/components/lib/sections/LogoWallGrid.astro +93 -0
  191. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  192. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  193. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  194. package/src/components/lib/sections/PricingSinglePlan.astro +159 -0
  195. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  196. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  197. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  198. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  199. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  200. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  201. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  202. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  203. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  204. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  205. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  206. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  207. package/src/components/lib/sections/StatsCounterBand.astro +124 -0
  208. package/src/components/lib/sections/StatsDashboardSection.astro +85 -0
  209. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  210. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  211. package/src/components/lib/sections/TestimonialCarousel.astro +167 -0
  212. package/src/components/lib/sections/TestimonialFeatured.astro +122 -0
  213. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  214. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  215. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  216. package/src/components/lib/sections/index.ts +39 -0
  217. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  218. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  219. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  220. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  221. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  222. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  223. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  224. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  225. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  226. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  227. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  228. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  229. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  230. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  231. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  232. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  233. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  234. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  235. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  236. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  237. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  238. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  239. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  240. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  241. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  242. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  243. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  244. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  245. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  246. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  247. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  248. package/src/components/lib/typography/index.ts +4 -0
  249. package/src/styles/global.css +113 -1
@@ -1,9 +1,16 @@
1
1
  ---
2
2
  /**
3
- * Galaxy3D — Interactive 3D galaxy background with mouse-reactive particle distortion.
3
+ * Galaxy3D — Interactive 3D galaxy background with a fluid, mouse-reactive particle field.
4
4
  *
5
- * Renders a spiral galaxy with thousands of particles using Three.js.
6
- * Mouse movement creates a repulsion force that distorts nearby particles in real-time.
5
+ * Renders a spiral galaxy with thousands of particles using Three.js. All particle motion
6
+ * (rotation, twinkle and cursor interaction) runs on the GPU in a vertex shader:
7
+ * the cursor parts the stars with a soft gaussian falloff, a gentle swirl and a wake
8
+ * that follows the pointer's direction, and nearby stars glow slightly brighter.
9
+ *
10
+ * Pointer tracking is frame-rate independent and listens on `window`, so overlays on top
11
+ * of the canvas (titles, buttons) don't create dead zones. Rendering pauses when the
12
+ * canvas is off-screen or the tab is hidden, and `prefers-reduced-motion` renders a
13
+ * static frame without interaction.
7
14
  *
8
15
  * @param {string} className - Additional CSS class
9
16
  */
@@ -14,7 +21,7 @@ interface Props {
14
21
  const { className = '' } = Astro.props;
15
22
  ---
16
23
 
17
- <div class="galaxy-3d-container {className}" id="galaxy-3d-wrapper" aria-hidden="true">
24
+ <div class={`galaxy-3d-container ${className}`} id="galaxy-3d-wrapper" aria-hidden="true">
18
25
  <canvas id="galaxy-3d-canvas"></canvas>
19
26
  </div>
20
27
 
@@ -45,28 +52,135 @@ const { className = '' } = Astro.props;
45
52
  const container = canvas.parentElement;
46
53
  if (!container) return;
47
54
 
55
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
56
+
48
57
  // ===== SCENE =====
49
58
  const scene = new THREE.Scene();
50
- const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100);
59
+ const FOV = 75;
60
+ const camera = new THREE.PerspectiveCamera(FOV, 1, 0.1, 100);
51
61
  camera.position.set(0, 0, 5);
52
62
 
53
63
  const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
54
64
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
55
65
 
66
+ // ===== PARTICLE SHADER =====
67
+ // Shared by the galaxy and the background stars. Positions are static; the shader
68
+ // rotates them, projects them, and displaces them away from the cursor in view space
69
+ // so the effect lines up with the pointer at any depth.
70
+ const vertexShader = /* glsl */ `
71
+ uniform float uTime;
72
+ uniform float uRotation;
73
+ uniform float uSize;
74
+ uniform float uScale;
75
+ uniform float uAspect;
76
+ uniform float uTanHalfFov;
77
+ uniform vec2 uMouse;
78
+ uniform vec2 uMouseVel;
79
+ uniform float uStrength;
80
+ uniform float uRadius;
81
+ uniform float uPush;
82
+
83
+ attribute vec3 color;
84
+ attribute float aRandom;
85
+
86
+ varying vec3 vColor;
87
+ varying float vAlpha;
88
+ varying float vGlow;
89
+
90
+ void main() {
91
+ // Spin around the galaxy's own axis.
92
+ float c = cos(uRotation);
93
+ float s = sin(uRotation);
94
+ vec3 p = vec3(position.x * c - position.z * s, position.y, position.x * s + position.z * c);
95
+
96
+ vec4 view = viewMatrix * modelMatrix * vec4(p, 1.0);
97
+ vec4 clip = projectionMatrix * view;
98
+ vec2 ndc = clip.xy / clip.w;
99
+
100
+ // Distance to the cursor in aspect-corrected screen units.
101
+ vec2 d = (ndc - uMouse) * vec2(uAspect, 1.0);
102
+ float dist = length(d);
103
+ float falloff = exp(-(dist * dist) / (uRadius * uRadius)) * uStrength;
104
+ vec2 dir = d / max(dist, 1e-4);
105
+ vec2 swirl = vec2(-dir.y, dir.x);
106
+
107
+ // Soft repulsion + a little swirl + a wake in the direction of travel.
108
+ // Each star reacts slightly differently so the field never moves as a rigid disc.
109
+ float personal = 0.75 + aRandom * 0.5;
110
+ vec2 push = (dir * 0.85 + swirl * 0.35) * falloff * personal + uMouseVel * falloff * 0.9;
111
+
112
+ // Screen units -> view units at this particle's depth.
113
+ // (d is measured in NDC-y units on both axes, and one NDC-y unit = tan(fov/2) * depth.)
114
+ view.xy += push * uPush * uTanHalfFov * -view.z;
115
+ gl_Position = projectionMatrix * view;
116
+
117
+ // Per-star twinkle with its own speed and phase.
118
+ float twinkle = 0.7 + 0.3 * sin(uTime * (0.6 + aRandom * 1.8) + aRandom * 6.2831);
119
+
120
+ vColor = color;
121
+ vGlow = falloff;
122
+ vAlpha = twinkle;
123
+ gl_PointSize = max(uSize * (0.55 + aRandom * 1.0) * (1.0 + falloff * 0.6) * uScale / -view.z, 1.0);
124
+ }
125
+ `;
126
+
127
+ const fragmentShader = /* glsl */ `
128
+ uniform float uOpacity;
129
+ varying vec3 vColor;
130
+ varying float vAlpha;
131
+ varying float vGlow;
132
+
133
+ void main() {
134
+ // Round, soft-edged star instead of a hard square.
135
+ float r = length(gl_PointCoord - 0.5);
136
+ float a = smoothstep(0.5, 0.05, r);
137
+ if (a <= 0.0) discard;
138
+ vec3 col = vColor * (1.0 + vGlow * 0.9);
139
+ gl_FragColor = vec4(col, a * vAlpha * uOpacity);
140
+ }
141
+ `;
142
+
143
+ // Pointer state shared by every particle layer.
144
+ const pointerUniforms = {
145
+ uMouse: { value: new THREE.Vector2(0, 0) },
146
+ uMouseVel: { value: new THREE.Vector2(0, 0) },
147
+ uStrength: { value: 0 },
148
+ uAspect: { value: 1 },
149
+ uScale: { value: 1 },
150
+ uTanHalfFov: { value: Math.tan(THREE.MathUtils.degToRad(FOV / 2)) },
151
+ uTime: { value: 0 },
152
+ };
153
+
154
+ function particleMaterial({ size, opacity, radius, push }) {
155
+ return new THREE.ShaderMaterial({
156
+ vertexShader,
157
+ fragmentShader,
158
+ transparent: true,
159
+ depthWrite: false,
160
+ blending: THREE.AdditiveBlending,
161
+ uniforms: {
162
+ ...pointerUniforms,
163
+ uRotation: { value: 0 },
164
+ uSize: { value: size },
165
+ uOpacity: { value: opacity },
166
+ uRadius: { value: radius },
167
+ uPush: { value: push },
168
+ },
169
+ });
170
+ }
171
+
56
172
  function resize() {
57
173
  const w = container.clientWidth;
58
174
  const h = container.clientHeight;
59
175
  if (w === 0 || h === 0) return;
60
- renderer.setSize(w, h);
176
+ renderer.setSize(w, h, false);
61
177
  camera.aspect = w / h;
62
178
  camera.updateProjectionMatrix();
179
+ pointerUniforms.uAspect.value = w / h;
180
+ pointerUniforms.uScale.value = renderer.domElement.height * 0.5;
181
+ if (reduceMotion) renderer.render(scene, camera);
63
182
  }
64
183
 
65
- requestAnimationFrame(() => resize());
66
- setTimeout(resize, 100);
67
- setTimeout(resize, 500);
68
- window.addEventListener('resize', resize);
69
-
70
184
  // ===== COLORS =====
71
185
  const palette = [
72
186
  new THREE.Color('#fef3c7'),
@@ -91,7 +205,7 @@ const { className = '' } = Astro.props;
91
205
  const galaxyGeometry = new THREE.BufferGeometry();
92
206
  const galaxyPositions = new Float32Array(GALAXY_COUNT * 3);
93
207
  const galaxyColors = new Float32Array(GALAXY_COUNT * 3);
94
- const galaxyOriginalPositions = new Float32Array(GALAXY_COUNT * 3);
208
+ const galaxyRandom = new Float32Array(GALAXY_COUNT);
95
209
 
96
210
  for (let i = 0; i < GALAXY_COUNT; i++) {
97
211
  const i3 = i * 3;
@@ -108,70 +222,56 @@ const { className = '' } = Astro.props;
108
222
  const angle = branchAngle + spinAngle;
109
223
  const spiralOffset = radius * galaxyParams.inwardDrop;
110
224
 
111
- const x = Math.cos(angle) * radius + randomX + Math.cos(angle + Math.PI / 2) * spiralOffset;
112
- const y = randomY;
113
- const z = Math.sin(angle) * radius + randomZ + Math.sin(angle + Math.PI / 2) * spiralOffset;
114
-
115
- galaxyPositions[i3] = x;
116
- galaxyPositions[i3 + 1] = y;
117
- galaxyPositions[i3 + 2] = z;
118
-
119
- galaxyOriginalPositions[i3] = x;
120
- galaxyOriginalPositions[i3 + 1] = y;
121
- galaxyOriginalPositions[i3 + 2] = z;
225
+ galaxyPositions[i3] = Math.cos(angle) * radius + randomX + Math.cos(angle + Math.PI / 2) * spiralOffset;
226
+ galaxyPositions[i3 + 1] = randomY;
227
+ galaxyPositions[i3 + 2] = Math.sin(angle) * radius + randomZ + Math.sin(angle + Math.PI / 2) * spiralOffset;
122
228
 
123
229
  const colorMix = radius / galaxyParams.radius;
124
230
  const baseColor = palette[Math.floor(Math.random() * 3)];
125
231
  const outerColor = palette[3 + Math.floor(Math.random() * 2)];
126
232
  const finalColor = baseColor.clone().lerp(outerColor, colorMix);
127
-
128
233
  galaxyColors[i3] = finalColor.r;
129
234
  galaxyColors[i3 + 1] = finalColor.g;
130
235
  galaxyColors[i3 + 2] = finalColor.b;
236
+
237
+ galaxyRandom[i] = Math.random();
131
238
  }
132
239
 
133
240
  galaxyGeometry.setAttribute('position', new THREE.BufferAttribute(galaxyPositions, 3));
134
241
  galaxyGeometry.setAttribute('color', new THREE.BufferAttribute(galaxyColors, 3));
242
+ galaxyGeometry.setAttribute('aRandom', new THREE.BufferAttribute(galaxyRandom, 1));
135
243
 
136
- const galaxyMaterial = new THREE.PointsMaterial({
137
- size: 0.008,
138
- sizeAttenuation: true,
139
- depthWrite: false,
140
- blending: THREE.AdditiveBlending,
141
- vertexColors: true,
142
- transparent: true,
143
- opacity: 0.95,
144
- });
145
-
244
+ const galaxyMaterial = particleMaterial({ size: 0.011, opacity: 0.95, radius: 0.19, push: 0.12 });
146
245
  const galaxyPoints = new THREE.Points(galaxyGeometry, galaxyMaterial);
147
246
  galaxyPoints.rotation.x = -0.15;
148
247
  galaxyPoints.rotation.z = 0.05;
149
248
  scene.add(galaxyPoints);
150
249
 
151
250
  // ===== BACKGROUND STARS =====
251
+ // Same shader with a much weaker push, so the deep field reacts subtly (parallax feel).
152
252
  const STAR_COUNT = 2000;
153
253
  const starGeometry = new THREE.BufferGeometry();
154
254
  const starPositions = new Float32Array(STAR_COUNT * 3);
255
+ const starColors = new Float32Array(STAR_COUNT * 3);
256
+ const starRandom = new Float32Array(STAR_COUNT);
257
+ const white = new THREE.Color('#ffffff');
155
258
 
156
259
  for (let i = 0; i < STAR_COUNT; i++) {
157
260
  const i3 = i * 3;
158
261
  starPositions[i3] = (Math.random() - 0.5) * 20;
159
262
  starPositions[i3 + 1] = (Math.random() - 0.5) * 20;
160
263
  starPositions[i3 + 2] = (Math.random() - 0.5) * 10 - 5;
264
+ starColors[i3] = white.r;
265
+ starColors[i3 + 1] = white.g;
266
+ starColors[i3 + 2] = white.b;
267
+ starRandom[i] = Math.random();
161
268
  }
162
269
 
163
270
  starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
271
+ starGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3));
272
+ starGeometry.setAttribute('aRandom', new THREE.BufferAttribute(starRandom, 1));
164
273
 
165
- const starMaterial = new THREE.PointsMaterial({
166
- size: 0.008,
167
- sizeAttenuation: true,
168
- depthWrite: false,
169
- transparent: true,
170
- opacity: 0.5,
171
- color: new THREE.Color('#ffffff'),
172
- blending: THREE.AdditiveBlending,
173
- });
174
-
274
+ const starMaterial = particleMaterial({ size: 0.012, opacity: 0.5, radius: 0.3, push: 0.05 });
175
275
  const stars = new THREE.Points(starGeometry, starMaterial);
176
276
  scene.add(stars);
177
277
 
@@ -236,100 +336,91 @@ const { className = '' } = Astro.props;
236
336
  ring.position.z = -0.3;
237
337
  scene.add(ring);
238
338
 
239
- // ===== MOUSE TRACKING =====
240
- const mouse = { x: 0, y: 0, worldX: 0, worldY: 0 };
241
- const targetMouse = { x: 0, y: 0 };
242
- const raycaster = new THREE.Raycaster();
243
- const mouseVec = new THREE.Vector2();
244
- const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
245
- const intersection = new THREE.Vector3();
339
+ requestAnimationFrame(() => resize());
340
+ setTimeout(resize, 100);
341
+ setTimeout(resize, 500);
342
+ window.addEventListener('resize', resize);
343
+
344
+ // Reduced motion: one static frame, no interaction or animation loop.
345
+ if (reduceMotion) {
346
+ renderer.render(scene, camera);
347
+ return;
348
+ }
246
349
 
247
- const MOUSE_RADIUS = 0.8;
248
- const MOUSE_FORCE = 0.25;
350
+ // ===== POINTER TRACKING =====
351
+ // Listen on window so the title/buttons layered over the canvas don't block it.
352
+ const target = { x: 0, y: 0, inside: false };
353
+ const smooth = { x: 0, y: 0 };
354
+ const vel = { x: 0, y: 0 };
355
+ let hasPointer = false;
356
+ let lastMove = 0;
249
357
 
250
- container.addEventListener('mousemove', (e) => {
358
+ window.addEventListener('pointermove', (e) => {
251
359
  const rect = canvas.getBoundingClientRect();
252
- targetMouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
253
- targetMouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
254
- });
360
+ const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
361
+ target.inside = inside && e.pointerType !== 'touch';
362
+ if (!target.inside) return;
363
+ target.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
364
+ target.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
365
+ if (!hasPointer) {
366
+ // First contact: start from the pointer instead of sliding in from the centre.
367
+ smooth.x = target.x;
368
+ smooth.y = target.y;
369
+ hasPointer = true;
370
+ }
371
+ lastMove = performance.now();
372
+ }, { passive: true });
255
373
 
256
- container.addEventListener('mouseleave', () => {
257
- targetMouse.x = 0;
258
- targetMouse.y = 0;
259
- });
374
+ document.addEventListener('pointerleave', () => { target.inside = false; });
375
+ window.addEventListener('blur', () => { target.inside = false; });
260
376
 
261
377
  // ===== ANIMATION =====
262
378
  const clock = new THREE.Clock();
263
-
264
- function animate() {
265
- requestAnimationFrame(animate);
266
- const elapsed = clock.getElapsedTime();
267
-
268
- // Ultra-smooth mouse interpolation
269
- mouse.x += (targetMouse.x - mouse.x) * 0.03;
270
- mouse.y += (targetMouse.y - mouse.y) * 0.03;
271
-
272
- // Convert screen mouse to world coordinates
273
- mouseVec.set(mouse.x, mouse.y);
274
- raycaster.setFromCamera(mouseVec, camera);
275
- raycaster.ray.intersectPlane(plane, intersection);
276
- mouse.worldX = intersection.x;
277
- mouse.worldY = intersection.y;
278
-
279
- // Rotate galaxy gently
280
- galaxyPoints.rotation.y = elapsed * 0.025;
281
-
282
- // Animate galaxy particles with gentle mouse interaction
283
- const pos = galaxyGeometry.attributes.position.array;
284
- for (let i = 0; i < GALAXY_COUNT; i++) {
285
- const i3 = i * 3;
286
-
287
- const ox = galaxyOriginalPositions[i3];
288
- const oy = galaxyOriginalPositions[i3 + 1];
289
- const oz = galaxyOriginalPositions[i3 + 2];
290
-
291
- // Apply rotation
292
- const ry = galaxyPoints.rotation.y;
293
- const cosR = Math.cos(ry);
294
- const sinR = Math.sin(ry);
295
- const baseX = ox * cosR - oz * sinR;
296
- const baseZ = ox * sinR + oz * cosR;
297
- const baseY = oy;
298
-
299
- // Subtle mouse deflection
300
- const dx = baseX - mouse.worldX;
301
- const dy = baseY - mouse.worldY;
302
- const dist = Math.sqrt(dx * dx + dy * dy);
303
-
304
- let pushX = 0, pushY = 0, pushZ = 0;
305
-
306
- if (dist < MOUSE_RADIUS && dist > 0.01) {
307
- const force = (1 - dist / MOUSE_RADIUS) * MOUSE_FORCE;
308
- const nx = dx / dist;
309
- const ny = dy / dist;
310
- pushX = nx * force;
311
- pushY = ny * force;
312
- pushZ = (nx * ny) * force * 0.15;
313
- }
314
-
315
- const targetX = baseX + pushX;
316
- const targetY = baseY + pushY;
317
- const targetZ = baseZ + pushZ;
318
-
319
- const spring = 0.03;
320
- pos[i3] += (targetX - pos[i3]) * spring;
321
- pos[i3 + 1] += (targetY - pos[i3 + 1]) * spring;
322
- pos[i3 + 2] += (targetZ - pos[i3 + 2]) * spring;
323
- }
324
- galaxyGeometry.attributes.position.needsUpdate = true;
325
-
326
- // Animate dust
379
+ let elapsed = 0;
380
+ let rafId = 0;
381
+ let visible = true;
382
+
383
+ // Frame-rate independent exponential smoothing.
384
+ const damp = (current, goal, lambda, dt) => current + (goal - current) * (1 - Math.exp(-lambda * dt));
385
+
386
+ function frame() {
387
+ rafId = requestAnimationFrame(frame);
388
+ const dt = Math.min(clock.getDelta(), 1 / 20);
389
+ elapsed += dt;
390
+ pointerUniforms.uTime.value = elapsed;
391
+
392
+ // Pointer: follows quickly but without jitter; velocity drives the wake.
393
+ const px = smooth.x;
394
+ const py = smooth.y;
395
+ smooth.x = damp(smooth.x, target.x, 9, dt);
396
+ smooth.y = damp(smooth.y, target.y, 9, dt);
397
+ const aspect = pointerUniforms.uAspect.value;
398
+ const rawVx = ((smooth.x - px) / Math.max(dt, 1e-4)) * aspect * 0.05;
399
+ const rawVy = ((smooth.y - py) / Math.max(dt, 1e-4)) * 0.05;
400
+ vel.x = damp(vel.x, rawVx, 6, dt);
401
+ vel.y = damp(vel.y, rawVy, 6, dt);
402
+ const vLen = Math.hypot(vel.x, vel.y);
403
+ const vMax = 0.6;
404
+ const vk = vLen > vMax ? vMax / vLen : 1;
405
+ pointerUniforms.uMouse.value.set(smooth.x, smooth.y);
406
+ pointerUniforms.uMouseVel.value.set(vel.x * vk, vel.y * vk);
407
+
408
+ // Influence eases in on hover, relaxes a little when the pointer rests, and fades out on leave.
409
+ const idle = performance.now() - lastMove > 1800;
410
+ const goalStrength = target.inside ? (idle ? 0.55 : 1) : 0;
411
+ pointerUniforms.uStrength.value = damp(pointerUniforms.uStrength.value, goalStrength, target.inside ? 3.5 : 1.8, dt);
412
+
413
+ // Galaxy spin (on the GPU) and a much slower drift for the deep field.
414
+ galaxyMaterial.uniforms.uRotation.value = elapsed * 0.025;
415
+ starMaterial.uniforms.uRotation.value = elapsed * 0.004;
416
+
417
+ // Dust drift.
327
418
  const dustPos = dustGeometry.attributes.position.array;
328
419
  for (let i = 0; i < DUST_COUNT; i++) {
329
420
  const i3 = i * 3;
330
- dustPos[i3] += dustVelocities[i3] * 0.002 + Math.sin(elapsed * 0.3 + i) * 0.0001;
331
- dustPos[i3 + 1] += dustVelocities[i3 + 1] * 0.002 + Math.cos(elapsed * 0.3 + i) * 0.0001;
332
- dustPos[i3 + 2] += dustVelocities[i3 + 2] * 0.002;
421
+ dustPos[i3] += dustVelocities[i3] * 0.12 * dt + Math.sin(elapsed * 0.3 + i) * 0.006 * dt;
422
+ dustPos[i3 + 1] += dustVelocities[i3 + 1] * 0.12 * dt + Math.cos(elapsed * 0.3 + i) * 0.006 * dt;
423
+ dustPos[i3 + 2] += dustVelocities[i3 + 2] * 0.12 * dt;
333
424
 
334
425
  if (dustPos[i3] > 4) dustPos[i3] = -4;
335
426
  if (dustPos[i3] < -4) dustPos[i3] = 4;
@@ -338,23 +429,44 @@ const { className = '' } = Astro.props;
338
429
  }
339
430
  dustGeometry.attributes.position.needsUpdate = true;
340
431
 
341
- // Central glow pulse
342
- const pulseScale = 1 + Math.sin(elapsed * 1.2) * 0.04;
432
+ // Central glow pulse.
433
+ const pulse = Math.sin(elapsed * 1.2);
434
+ const pulseScale = 1 + pulse * 0.04;
343
435
  glow.scale.set(pulseScale, pulseScale, pulseScale);
344
- glowMat.opacity = 0.12 + Math.sin(elapsed * 1.2) * 0.02;
436
+ glowMat.opacity = 0.12 + pulse * 0.02;
345
437
 
346
- // Ring rotation
438
+ // Ring rotation.
347
439
  ring.rotation.z = elapsed * 0.08;
348
440
  ringMat.opacity = 0.06 + Math.sin(elapsed * 1.5) * 0.015;
349
441
 
350
- // Gentle camera mouse parallax
351
- camera.position.x += (mouse.x * 0.15 - camera.position.x) * 0.015;
352
- camera.position.y += (mouse.y * 0.1 - camera.position.y) * 0.015;
442
+ // Gentle camera parallax that follows the pointer only while it is over the scene.
443
+ const s = pointerUniforms.uStrength.value;
444
+ camera.position.x = damp(camera.position.x, smooth.x * 0.18 * s, 2.2, dt);
445
+ camera.position.y = damp(camera.position.y, smooth.y * 0.12 * s, 2.2, dt);
353
446
  camera.lookAt(0, 0, 0);
354
447
 
355
448
  renderer.render(scene, camera);
356
449
  }
357
450
 
358
- animate();
451
+ function start() {
452
+ if (rafId || !visible || document.hidden) return;
453
+ clock.getDelta(); // drop the paused interval
454
+ rafId = requestAnimationFrame(frame);
455
+ }
456
+ function stop() {
457
+ cancelAnimationFrame(rafId);
458
+ rafId = 0;
459
+ }
460
+
461
+ // Pause when off-screen or when the tab is hidden.
462
+ if ('IntersectionObserver' in window) {
463
+ new IntersectionObserver((entries) => {
464
+ visible = entries[0].isIntersecting;
465
+ visible ? start() : stop();
466
+ }).observe(container);
467
+ }
468
+ document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()));
469
+
470
+ start();
359
471
  })();
360
- </script>
472
+ </script>