From eb210bd17c84224597a7f1496bc573d258cdb023 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 Aug 2025 19:32:50 +0000 Subject: [PATCH] Add Galaxy component with dynamic star background and interactive effects Co-authored-by: rikkyrich69 --- client/src/components/Galaxy.tsx | 363 +++++++++++++++++++++++++++ client/src/components/GalaxyDemo.tsx | 67 +++++ client/src/pages/Home.tsx | 26 +- package-lock.json | 7 + package.json | 1 + 5 files changed, 460 insertions(+), 4 deletions(-) create mode 100644 client/src/components/Galaxy.tsx create mode 100644 client/src/components/GalaxyDemo.tsx diff --git a/client/src/components/Galaxy.tsx b/client/src/components/Galaxy.tsx new file mode 100644 index 0000000..be86425 --- /dev/null +++ b/client/src/components/Galaxy.tsx @@ -0,0 +1,363 @@ +import { Renderer, Program, Mesh, Color, Triangle } from "ogl"; +import { useEffect, useRef } from "react"; + +const vertexShader = ` +attribute vec2 uv; +attribute vec2 position; + +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = vec4(position, 0, 1); +} +`; + +const fragmentShader = ` +precision highp float; + +uniform float uTime; +uniform vec3 uResolution; +uniform vec2 uFocal; +uniform vec2 uRotation; +uniform float uStarSpeed; +uniform float uDensity; +uniform float uHueShift; +uniform float uSpeed; +uniform vec2 uMouse; +uniform float uGlowIntensity; +uniform float uSaturation; +uniform bool uMouseRepulsion; +uniform float uTwinkleIntensity; +uniform float uRotationSpeed; +uniform float uRepulsionStrength; +uniform float uMouseActiveFactor; +uniform float uAutoCenterRepulsion; +uniform bool uTransparent; + +varying vec2 vUv; + +#define NUM_LAYER 4.0 +#define STAR_COLOR_CUTOFF 0.2 +#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071) +#define PERIOD 3.0 + +float Hash21(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float tri(float x) { + return abs(fract(x) * 2.0 - 1.0); +} + +float tris(float x) { + float t = fract(x); + return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0)); +} + +float trisn(float x) { + float t = fract(x); + return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0; +} + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +float Star(vec2 uv, float flare) { + float d = length(uv); + float m = (0.05 * uGlowIntensity) / d; + float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0)); + m += rays * flare * uGlowIntensity; + uv *= MAT45; + rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0)); + m += rays * 0.3 * flare * uGlowIntensity; + m *= smoothstep(1.0, 0.2, d); + return m; +} + +vec3 StarLayer(vec2 uv) { + vec3 col = vec3(0.0); + + vec2 gv = fract(uv) - 0.5; + vec2 id = floor(uv); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 offset = vec2(float(x), float(y)); + vec2 si = id + vec2(float(x), float(y)); + float seed = Hash21(si); + float size = fract(seed * 345.32); + float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0)); + float flareSize = smoothstep(0.9, 1.0, size) * glossLocal; + + float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF; + float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF; + float grn = min(red, blu) * seed; + vec3 base = vec3(red, grn, blu); + + float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5; + hue = fract(hue + uHueShift / 360.0); + float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation; + float val = max(max(base.r, base.g), base.b); + base = hsv2rgb(vec3(hue, sat, val)); + + vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5; + + float star = Star(gv - offset - pad, flareSize); + vec3 color = base; + + float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0; + twinkle = mix(1.0, twinkle, uTwinkleIntensity); + star *= twinkle; + + col += star * size * color; + } + } + + return col; +} + +void main() { + vec2 focalPx = uFocal * uResolution.xy; + vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y; + + vec2 mouseNorm = uMouse - vec2(0.5); + + if (uAutoCenterRepulsion > 0.0) { + vec2 centerUV = vec2(0.0, 0.0); // Center in UV space + float centerDist = length(uv - centerUV); + vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1)); + uv += repulsion * 0.05; + } else if (uMouseRepulsion) { + vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y; + float mouseDist = length(uv - mousePosUV); + vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1)); + uv += repulsion * 0.05 * uMouseActiveFactor; + } else { + vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor; + uv += mouseOffset; + } + + float autoRotAngle = uTime * uRotationSpeed; + mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle)); + uv = autoRot * uv; + + uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv; + + vec3 col = vec3(0.0); + + for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) { + float depth = fract(i + uStarSpeed * uSpeed); + float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth); + float fade = depth * smoothstep(1.0, 0.9, depth); + col += StarLayer(uv * scale + i * 453.32) * fade; + } + + if (uTransparent) { + float alpha = length(col); + alpha = smoothstep(0.0, 0.3, alpha); // Enhance contrast + alpha = min(alpha, 1.0); // Clamp to maximum 1.0 + gl_FragColor = vec4(col, alpha); + } else { + gl_FragColor = vec4(col, 1.0); + } +} +`; + +interface GalaxyProps { + focal?: [number, number]; + rotation?: [number, number]; + starSpeed?: number; + density?: number; + hueShift?: number; + disableAnimation?: boolean; + speed?: number; + mouseInteraction?: boolean; + glowIntensity?: number; + saturation?: number; + mouseRepulsion?: boolean; + twinkleIntensity?: number; + rotationSpeed?: number; + repulsionStrength?: number; + autoCenterRepulsion?: number; + transparent?: boolean; + className?: string; +} + +export default function Galaxy({ + focal = [0.5, 0.5], + rotation = [1.0, 0.0], + starSpeed = 0.5, + density = 1, + hueShift = 140, + disableAnimation = false, + speed = 1.0, + mouseInteraction = true, + glowIntensity = 0.3, + saturation = 0.0, + mouseRepulsion = true, + repulsionStrength = 2, + twinkleIntensity = 0.3, + rotationSpeed = 0.1, + autoCenterRepulsion = 0, + transparent = true, + className = "", + ...rest +}: GalaxyProps) { + const ctnDom = useRef(null); + const targetMousePos = useRef({ x: 0.5, y: 0.5 }); + const smoothMousePos = useRef({ x: 0.5, y: 0.5 }); + const targetMouseActive = useRef(0.0); + const smoothMouseActive = useRef(0.0); + + useEffect(() => { + if (!ctnDom.current) return; + const ctn = ctnDom.current; + const renderer = new Renderer({ + alpha: transparent, + premultipliedAlpha: false, + }); + const gl = renderer.gl; + + if (transparent) { + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.clearColor(0, 0, 0, 0); + } else { + gl.clearColor(0, 0, 0, 1); + } + + let program: Program; + + function resize() { + const scale = 1; + renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale); + if (program) { + program.uniforms.uResolution.value = new Color( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ); + } + } + window.addEventListener("resize", resize, false); + resize(); + + const geometry = new Triangle(gl); + program = new Program(gl, { + vertex: vertexShader, + fragment: fragmentShader, + uniforms: { + uTime: { value: 0 }, + uResolution: { + value: new Color( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ), + }, + uFocal: { value: new Float32Array(focal) }, + uRotation: { value: new Float32Array(rotation) }, + uStarSpeed: { value: starSpeed }, + uDensity: { value: density }, + uHueShift: { value: hueShift }, + uSpeed: { value: speed }, + uMouse: { + value: new Float32Array([ + smoothMousePos.current.x, + smoothMousePos.current.y, + ]), + }, + uGlowIntensity: { value: glowIntensity }, + uSaturation: { value: saturation }, + uMouseRepulsion: { value: mouseRepulsion }, + uTwinkleIntensity: { value: twinkleIntensity }, + uRotationSpeed: { value: rotationSpeed }, + uRepulsionStrength: { value: repulsionStrength }, + uMouseActiveFactor: { value: 0.0 }, + uAutoCenterRepulsion: { value: autoCenterRepulsion }, + uTransparent: { value: transparent }, + }, + }); + + const mesh = new Mesh(gl, { geometry, program }); + let animateId: number; + + function update(t: number) { + animateId = requestAnimationFrame(update); + if (!disableAnimation) { + program.uniforms.uTime.value = t * 0.001; + program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0; + } + + const lerpFactor = 0.05; + smoothMousePos.current.x += + (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor; + smoothMousePos.current.y += + (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor; + + smoothMouseActive.current += + (targetMouseActive.current - smoothMouseActive.current) * lerpFactor; + + program.uniforms.uMouse.value[0] = smoothMousePos.current.x; + program.uniforms.uMouse.value[1] = smoothMousePos.current.y; + program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current; + + renderer.render({ scene: mesh }); + } + animateId = requestAnimationFrame(update); + ctn.appendChild(gl.canvas); + + function handleMouseMove(e: MouseEvent) { + const rect = ctn.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width; + const y = 1.0 - (e.clientY - rect.top) / rect.height; + targetMousePos.current = { x, y }; + targetMouseActive.current = 1.0; + } + + function handleMouseLeave() { + targetMouseActive.current = 0.0; + } + + if (mouseInteraction) { + ctn.addEventListener("mousemove", handleMouseMove); + ctn.addEventListener("mouseleave", handleMouseLeave); + } + + return () => { + cancelAnimationFrame(animateId); + window.removeEventListener("resize", resize); + if (mouseInteraction) { + ctn.removeEventListener("mousemove", handleMouseMove); + ctn.removeEventListener("mouseleave", handleMouseLeave); + } + ctn.removeChild(gl.canvas); + gl.getExtension("WEBGL_lose_context")?.loseContext(); + }; + }, [ + focal, + rotation, + starSpeed, + density, + hueShift, + disableAnimation, + speed, + mouseInteraction, + glowIntensity, + saturation, + mouseRepulsion, + twinkleIntensity, + rotationSpeed, + repulsionStrength, + autoCenterRepulsion, + transparent, + ]); + + return
; +} \ No newline at end of file diff --git a/client/src/components/GalaxyDemo.tsx b/client/src/components/GalaxyDemo.tsx new file mode 100644 index 0000000..71df1a3 --- /dev/null +++ b/client/src/components/GalaxyDemo.tsx @@ -0,0 +1,67 @@ +import Galaxy from "./Galaxy"; + +interface GalaxyDemoProps { + variant?: "hero" | "subtle" | "intense" | "custom"; + className?: string; +} + +export default function GalaxyDemo({ variant = "hero", className = "" }: GalaxyDemoProps) { + const configurations = { + hero: { + density: 0.8, + starSpeed: 0.3, + hueShift: 200, + glowIntensity: 0.4, + twinkleIntensity: 0.5, + rotationSpeed: 0.05, + mouseInteraction: true, + mouseRepulsion: true, + repulsionStrength: 1.5, + transparent: true, + }, + subtle: { + density: 0.5, + starSpeed: 0.1, + hueShift: 180, + glowIntensity: 0.2, + twinkleIntensity: 0.3, + rotationSpeed: 0.02, + mouseInteraction: false, + mouseRepulsion: false, + repulsionStrength: 0, + transparent: true, + }, + intense: { + density: 1.2, + starSpeed: 0.8, + hueShift: 240, + glowIntensity: 0.6, + twinkleIntensity: 0.7, + rotationSpeed: 0.15, + mouseInteraction: true, + mouseRepulsion: true, + repulsionStrength: 2.5, + transparent: true, + }, + custom: { + density: 1.0, + starSpeed: 0.4, + hueShift: 160, + glowIntensity: 0.5, + twinkleIntensity: 0.4, + rotationSpeed: 0.08, + mouseInteraction: true, + mouseRepulsion: true, + repulsionStrength: 1.8, + transparent: true, + }, + }; + + const config = configurations[variant]; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index d1572a8..9c66c1a 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { motion } from "framer-motion"; import AnimatedSection from "@/components/AnimatedSection"; import PageTransition from "@/components/PageTransition"; +import Galaxy from "@/components/Galaxy"; const stats = [ { value: "15+", label: "Projects Completed" }, @@ -17,9 +18,26 @@ export default function Home() {
{/* Hero Section */} -
-
-
+
+ {/* Galaxy Background */} +
+ +
+ {/* Overlay for better text readability */} +
+
+
diff --git a/package-lock.json b/package-lock.json index ae15b87..e20f4e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "lucide-react": "^0.453.0", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "ogl": "^1.0.11", "passport": "^0.7.0", "passport-local": "^1.0.0", "react": "^18.3.1", @@ -6685,6 +6686,12 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "license": "MIT" }, + "node_modules/ogl": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz", + "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==", + "license": "Unlicense" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 05df157..c0a8810 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lucide-react": "^0.453.0", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "ogl": "^1.0.11", "passport": "^0.7.0", "passport-local": "^1.0.0", "react": "^18.3.1",