From 85bdc5caedcde021951bbe73605a8ec0d34f6ee2 Mon Sep 17 00:00:00 2001 From: Jesus Ordosgoitty Date: Sat, 29 Nov 2025 23:52:52 -0400 Subject: [PATCH 1/4] Add ColorBends component and integrate into Hero section; update component aliases and dependencies --- components.json | 4 + package.json | 1 + pnpm-lock.yaml | 8 + src/components/ColorBends.tsx | 308 ++++++++++++++++++++++++++++++++++ src/components/Hero.tsx | 7 +- 5 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 src/components/ColorBends.tsx diff --git a/components.json b/components.json index 62e1011..e7b1f03 100644 --- a/components.json +++ b/components.json @@ -10,11 +10,15 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" + }, + "registries": { + "@react-bits": "https://reactbits.dev/r/{name}.json" } } diff --git a/package.json b/package.json index cc92f22..435e50d 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", + "three": "^0.181.2", "vaul": "^0.9.9", "zod": "^3.25.76" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5958d8..cc34860 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + three: + specifier: ^0.181.2 + version: 0.181.2 vaul: specifier: ^0.9.9 version: 0.9.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2531,6 +2534,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three@0.181.2: + resolution: {integrity: sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -4895,6 +4901,8 @@ snapshots: dependencies: any-promise: 1.3.0 + three@0.181.2: {} + tiny-invariant@1.3.3: {} to-regex-range@5.0.1: diff --git a/src/components/ColorBends.tsx b/src/components/ColorBends.tsx new file mode 100644 index 0000000..36bcc72 --- /dev/null +++ b/src/components/ColorBends.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useRef } from 'react'; +import * as THREE from 'three'; + +type ColorBendsProps = { + className?: string; + style?: React.CSSProperties; + rotation?: number; + speed?: number; + colors?: string[]; + transparent?: boolean; + autoRotate?: number; + scale?: number; + frequency?: number; + warpStrength?: number; + mouseInfluence?: number; + parallax?: number; + noise?: number; +}; + +const MAX_COLORS = 8 as const; + +const frag = ` +#define MAX_COLORS ${MAX_COLORS} +uniform vec2 uCanvas; +uniform float uTime; +uniform float uSpeed; +uniform vec2 uRot; +uniform int uColorCount; +uniform vec3 uColors[MAX_COLORS]; +uniform int uTransparent; +uniform float uScale; +uniform float uFrequency; +uniform float uWarpStrength; +uniform vec2 uPointer; // in NDC [-1,1] +uniform float uMouseInfluence; +uniform float uParallax; +uniform float uNoise; +varying vec2 vUv; + +void main() { + float t = uTime * uSpeed; + vec2 p = vUv * 2.0 - 1.0; + p += uPointer * uParallax * 0.1; + vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x); + vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y); + q /= max(uScale, 0.0001); + q /= 0.5 + 0.2 * dot(q, q); + q += 0.2 * cos(t) - 7.56; + vec2 toward = (uPointer - rp); + q += toward * uMouseInfluence * 0.2; + + vec3 col = vec3(0.0); + float a = 1.0; + + if (uColorCount > 0) { + vec2 s = q; + vec3 sumCol = vec3(0.0); + float cover = 0.0; + for (int i = 0; i < MAX_COLORS; ++i) { + if (i >= uColorCount) break; + s -= 0.01; + vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency)); + float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0); + float kBelow = clamp(uWarpStrength, 0.0, 1.0); + float kMix = pow(kBelow, 0.3); // strong response across 0..1 + float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement + vec2 disp = (r - s) * kBelow; + vec2 warped = s + disp * gain; + float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0); + float m = mix(m0, m1, kMix); + float w = 1.0 - exp(-6.0 / exp(6.0 * m)); + sumCol += uColors[i] * w; + cover = max(cover, w); + } + col = clamp(sumCol, 0.0, 1.0); + a = uTransparent > 0 ? cover : 1.0; + } else { + vec2 s = q; + for (int k = 0; k < 3; ++k) { + s -= 0.01; + vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency)); + float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0); + float kBelow = clamp(uWarpStrength, 0.0, 1.0); + float kMix = pow(kBelow, 0.3); + float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); + vec2 disp = (r - s) * kBelow; + vec2 warped = s + disp * gain; + float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0); + float m = mix(m0, m1, kMix); + col[k] = 1.0 - exp(-6.0 / exp(6.0 * m)); + } + a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0; + } + + if (uNoise > 0.0001) { + float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123); + col += (n - 0.5) * uNoise; + col = clamp(col, 0.0, 1.0); + } + + vec3 rgb = (uTransparent > 0) ? col * a : col; + gl_FragColor = vec4(rgb, a); +} +`; + +const vert = ` +varying vec2 vUv; +void main() { + vUv = uv; + gl_Position = vec4(position, 1.0); +} +`; + +export default function ColorBends({ + className, + style, + rotation = 45, + speed = 0.2, + colors = [], + transparent = true, + autoRotate = 0, + scale = 1, + frequency = 1, + warpStrength = 1, + mouseInfluence = 1, + parallax = 0.5, + noise = 0.1 +}: ColorBendsProps) { + const containerRef = useRef(null); + const rendererRef = useRef(null); + const rafRef = useRef(null); + const materialRef = useRef(null); + const resizeObserverRef = useRef(null); + const rotationRef = useRef(rotation); + const autoRotateRef = useRef(autoRotate); + const pointerTargetRef = useRef(new THREE.Vector2(0, 0)); + const pointerCurrentRef = useRef(new THREE.Vector2(0, 0)); + const pointerSmoothRef = useRef(8); + + useEffect(() => { + const container = containerRef.current!; + const scene = new THREE.Scene(); + const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + + const geometry = new THREE.PlaneGeometry(2, 2); + const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0)); + const material = new THREE.ShaderMaterial({ + vertexShader: vert, + fragmentShader: frag, + uniforms: { + uCanvas: { value: new THREE.Vector2(1, 1) }, + uTime: { value: 0 }, + uSpeed: { value: speed }, + uRot: { value: new THREE.Vector2(1, 0) }, + uColorCount: { value: 0 }, + uColors: { value: uColorsArray }, + uTransparent: { value: transparent ? 1 : 0 }, + uScale: { value: scale }, + uFrequency: { value: frequency }, + uWarpStrength: { value: warpStrength }, + uPointer: { value: new THREE.Vector2(0, 0) }, + uMouseInfluence: { value: mouseInfluence }, + uParallax: { value: parallax }, + uNoise: { value: noise } + }, + premultipliedAlpha: true, + transparent: true + }); + materialRef.current = material; + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + const renderer = new THREE.WebGLRenderer({ + antialias: false, + powerPreference: 'high-performance', + alpha: true + }); + rendererRef.current = renderer; + (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace; + renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + renderer.setClearColor(0x000000, transparent ? 0 : 1); + renderer.domElement.style.width = '100%'; + renderer.domElement.style.height = '100%'; + renderer.domElement.style.display = 'block'; + container.appendChild(renderer.domElement); + + const clock = new THREE.Clock(); + + const handleResize = () => { + const w = container.clientWidth || 1; + const h = container.clientHeight || 1; + renderer.setSize(w, h, false); + (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h); + }; + + handleResize(); + + if ('ResizeObserver' in window) { + const ro = new ResizeObserver(handleResize); + ro.observe(container); + resizeObserverRef.current = ro; + } else { + (window as Window).addEventListener('resize', handleResize); + } + + const loop = () => { + const dt = clock.getDelta(); + const elapsed = clock.elapsedTime; + material.uniforms.uTime.value = elapsed; + + const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed; + const rad = (deg * Math.PI) / 180; + const c = Math.cos(rad); + const s = Math.sin(rad); + (material.uniforms.uRot.value as THREE.Vector2).set(c, s); + + const cur = pointerCurrentRef.current; + const tgt = pointerTargetRef.current; + const amt = Math.min(1, dt * pointerSmoothRef.current); + cur.lerp(tgt, amt); + (material.uniforms.uPointer.value as THREE.Vector2).copy(cur); + renderer.render(scene, camera); + rafRef.current = requestAnimationFrame(loop); + }; + rafRef.current = requestAnimationFrame(loop); + + return () => { + if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); + if (resizeObserverRef.current) resizeObserverRef.current.disconnect(); + else (window as Window).removeEventListener('resize', handleResize); + geometry.dispose(); + material.dispose(); + renderer.dispose(); + if (renderer.domElement && renderer.domElement.parentElement === container) { + container.removeChild(renderer.domElement); + } + }; + }, []); + + useEffect(() => { + const material = materialRef.current; + const renderer = rendererRef.current; + if (!material) return; + + rotationRef.current = rotation; + autoRotateRef.current = autoRotate; + material.uniforms.uSpeed.value = speed; + material.uniforms.uScale.value = scale; + material.uniforms.uFrequency.value = frequency; + material.uniforms.uWarpStrength.value = warpStrength; + material.uniforms.uMouseInfluence.value = mouseInfluence; + material.uniforms.uParallax.value = parallax; + material.uniforms.uNoise.value = noise; + + const toVec3 = (hex: string) => { + const h = hex.replace('#', '').trim(); + const v = + h.length === 3 + ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)] + : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; + return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255); + }; + + const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3); + for (let i = 0; i < MAX_COLORS; i++) { + const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i]; + if (i < arr.length) vec.copy(arr[i]); + else vec.set(0, 0, 0); + } + material.uniforms.uColorCount.value = arr.length; + + material.uniforms.uTransparent.value = transparent ? 1 : 0; + if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1); + }, [ + rotation, + autoRotate, + speed, + scale, + frequency, + warpStrength, + mouseInfluence, + parallax, + noise, + colors, + transparent + ]); + + useEffect(() => { + const material = materialRef.current; + const container = containerRef.current; + if (!material || !container) return; + + const handlePointerMove = (e: PointerEvent) => { + const rect = container.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1; + const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1); + pointerTargetRef.current.set(x, y); + }; + + container.addEventListener('pointermove', handlePointerMove); + return () => { + container.removeEventListener('pointermove', handlePointerMove); + }; + }, []); + + return
; +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index fef2191..d2b1c9d 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -2,6 +2,7 @@ import { ChevronDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import Video from './Video'; import { useTranslation } from 'react-i18next'; +import ColorBends from './ColorBends'; const Hero = () => { const { t } = useTranslation(); @@ -15,9 +16,9 @@ const Hero = () => { return (
-