Spaces:
Sleeping
Sleeping
| import React, { useRef, useEffect, useState } from 'react'; | |
| import { Button } from './button'; | |
| // Types for component props | |
| interface HeroProps { | |
| trustBadge?: { | |
| text: string; | |
| icons?: string[]; | |
| }; | |
| headline: { | |
| line1: string; | |
| line2: string; | |
| }; | |
| subtitle: string; | |
| buttons?: { | |
| primary?: { | |
| text: string; | |
| onClick?: () => void; | |
| }; | |
| secondary?: { | |
| text: string; | |
| onClick?: () => void; | |
| }; | |
| }; | |
| className?: string; | |
| } | |
| // Reusable Shader Background Hook | |
| const useShaderBackground = () => { | |
| const canvasRef = useRef<HTMLCanvasElement | null>(null); | |
| const animationFrameRef = useRef<number | null>(null); | |
| const rendererRef = useRef<WebGLRenderer | null>(null); | |
| const pointersRef = useRef<PointerHandler | null>(null); | |
| // WebGL Renderer class | |
| class WebGLRenderer { | |
| private canvas: HTMLCanvasElement; | |
| private gl: WebGL2RenderingContext; | |
| private program: WebGLProgram | null = null; | |
| private vs: WebGLShader | null = null; | |
| private fs: WebGLShader | null = null; | |
| private buffer: WebGLBuffer | null = null; | |
| private scale: number; | |
| private shaderSource: string; | |
| private mouseMove: [number, number] = [0, 0]; | |
| private mouseCoords: [number, number] = [0, 0]; | |
| private pointerCoords: number[] = [0, 0]; | |
| private nbrOfPointers = 0; | |
| private vertexSrc = `#version 300 es | |
| precision highp float; | |
| in vec4 position; | |
| void main(){gl_Position=position;}`; | |
| private vertices = [-1, 1, -1, -1, 1, 1, 1, -1]; | |
| constructor(canvas: HTMLCanvasElement, scale: number) { | |
| this.canvas = canvas; | |
| this.scale = scale; | |
| this.gl = canvas.getContext('webgl2')!; | |
| this.gl.viewport(0, 0, canvas.width * scale, canvas.height * scale); | |
| this.shaderSource = defaultShaderSource; | |
| } | |
| updateShader(source: string) { | |
| this.reset(); | |
| this.shaderSource = source; | |
| this.setup(); | |
| this.init(); | |
| } | |
| updateMove(deltas: [number, number]) { | |
| this.mouseMove = deltas; | |
| } | |
| updateMouse(coords: [number, number]) { | |
| this.mouseCoords = coords; | |
| } | |
| updatePointerCoords(coords: number[]) { | |
| this.pointerCoords = coords; | |
| } | |
| updatePointerCount(nbr: number) { | |
| this.nbrOfPointers = nbr; | |
| } | |
| updateScale(scale: number) { | |
| this.scale = scale; | |
| this.gl.viewport(0, 0, this.canvas.width * scale, this.canvas.height * scale); | |
| } | |
| compile(shader: WebGLShader, source: string) { | |
| const gl = this.gl; | |
| gl.shaderSource(shader, source); | |
| gl.compileShader(shader); | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| const error = gl.getShaderInfoLog(shader); | |
| console.error('Shader compilation error:', error); | |
| } | |
| } | |
| test(source: string) { | |
| let result = null; | |
| const gl = this.gl; | |
| const shader = gl.createShader(gl.FRAGMENT_SHADER)!; | |
| gl.shaderSource(shader, source); | |
| gl.compileShader(shader); | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| result = gl.getShaderInfoLog(shader); | |
| } | |
| gl.deleteShader(shader); | |
| return result; | |
| } | |
| reset() { | |
| const gl = this.gl; | |
| if (this.program && !gl.getProgramParameter(this.program, gl.DELETE_STATUS)) { | |
| if (this.vs) { | |
| gl.detachShader(this.program, this.vs); | |
| gl.deleteShader(this.vs); | |
| } | |
| if (this.fs) { | |
| gl.detachShader(this.program, this.fs); | |
| gl.deleteShader(this.fs); | |
| } | |
| gl.deleteProgram(this.program); | |
| } | |
| } | |
| setup() { | |
| const gl = this.gl; | |
| this.vs = gl.createShader(gl.VERTEX_SHADER)!; | |
| this.fs = gl.createShader(gl.FRAGMENT_SHADER)!; | |
| this.compile(this.vs, this.vertexSrc); | |
| this.compile(this.fs, this.shaderSource); | |
| this.program = gl.createProgram()!; | |
| gl.attachShader(this.program, this.vs); | |
| gl.attachShader(this.program, this.fs); | |
| gl.linkProgram(this.program); | |
| if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { | |
| console.error(gl.getProgramInfoLog(this.program)); | |
| } | |
| } | |
| init() { | |
| const gl = this.gl; | |
| const program = this.program!; | |
| this.buffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertices), gl.STATIC_DRAW); | |
| const position = gl.getAttribLocation(program, 'position'); | |
| gl.enableVertexAttribArray(position); | |
| gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); | |
| (program as any).resolution = gl.getUniformLocation(program, 'resolution'); | |
| (program as any).time = gl.getUniformLocation(program, 'time'); | |
| (program as any).move = gl.getUniformLocation(program, 'move'); | |
| (program as any).touch = gl.getUniformLocation(program, 'touch'); | |
| (program as any).pointerCount = gl.getUniformLocation(program, 'pointerCount'); | |
| (program as any).pointers = gl.getUniformLocation(program, 'pointers'); | |
| } | |
| render(now = 0) { | |
| const gl = this.gl; | |
| const program = this.program; | |
| if (!program || gl.getProgramParameter(program, gl.DELETE_STATUS)) return; | |
| gl.clearColor(0, 0, 0, 1); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.useProgram(program); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); | |
| gl.uniform2f((program as any).resolution, this.canvas.width, this.canvas.height); | |
| gl.uniform1f((program as any).time, now * 1e-3); | |
| gl.uniform2f((program as any).move, ...this.mouseMove); | |
| gl.uniform2f((program as any).touch, ...this.mouseCoords); | |
| gl.uniform1i((program as any).pointerCount, this.nbrOfPointers); | |
| gl.uniform2fv((program as any).pointers, this.pointerCoords); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| } | |
| } | |
| // Pointer Handler class | |
| class PointerHandler { | |
| private scale: number; | |
| private active = false; | |
| private pointers = new Map<number, [number, number]>(); | |
| private lastCoords: [number, number] = [0, 0]; | |
| private moves: [number, number] = [0, 0]; | |
| constructor(element: HTMLCanvasElement, scale: number) { | |
| this.scale = scale; | |
| const map = (element: HTMLCanvasElement, scale: number, x: number, y: number): [number, number] => | |
| [x * scale, element.height - y * scale]; | |
| element.addEventListener('pointerdown', (e) => { | |
| this.active = true; | |
| this.pointers.set(e.pointerId, map(element, this.getScale(), e.clientX, e.clientY)); | |
| }); | |
| element.addEventListener('pointerup', (e) => { | |
| if (this.count === 1) { | |
| this.lastCoords = this.first; | |
| } | |
| this.pointers.delete(e.pointerId); | |
| this.active = this.pointers.size > 0; | |
| }); | |
| element.addEventListener('pointerleave', (e) => { | |
| if (this.count === 1) { | |
| this.lastCoords = this.first; | |
| } | |
| this.pointers.delete(e.pointerId); | |
| this.active = this.pointers.size > 0; | |
| }); | |
| element.addEventListener('pointermove', (e) => { | |
| if (!this.active) return; | |
| this.lastCoords = [e.clientX, e.clientY]; | |
| this.pointers.set(e.pointerId, map(element, this.getScale(), e.clientX, e.clientY)); | |
| this.moves = [this.moves[0] + e.movementX, this.moves[1] + e.movementY]; | |
| }); | |
| } | |
| getScale() { | |
| return this.scale; | |
| } | |
| updateScale(scale: number) { | |
| this.scale = scale; | |
| } | |
| get count() { | |
| return this.pointers.size; | |
| } | |
| get move(): [number, number] { | |
| return this.moves; | |
| } | |
| get coords(): number[] { | |
| return this.pointers.size > 0 | |
| ? Array.from(this.pointers.values()).flat() | |
| : [0, 0]; | |
| } | |
| get first(): [number, number] { | |
| return (this.pointers.values().next().value as [number, number]) || this.lastCoords; | |
| } | |
| } | |
| const resize = () => { | |
| if (!canvasRef.current) return; | |
| const canvas = canvasRef.current; | |
| const dpr = Math.max(1, 0.5 * window.devicePixelRatio); | |
| canvas.width = window.innerWidth * dpr; | |
| canvas.height = window.innerHeight * dpr; | |
| if (rendererRef.current) { | |
| rendererRef.current.updateScale(dpr); | |
| } | |
| }; | |
| const loop = (now: number) => { | |
| if (!rendererRef.current || !pointersRef.current) return; | |
| rendererRef.current.updateMouse(pointersRef.current.first); | |
| rendererRef.current.updatePointerCount(pointersRef.current.count); | |
| rendererRef.current.updatePointerCoords(pointersRef.current.coords); | |
| rendererRef.current.updateMove(pointersRef.current.move); | |
| rendererRef.current.render(now); | |
| animationFrameRef.current = requestAnimationFrame(loop); | |
| }; | |
| useEffect(() => { | |
| if (!canvasRef.current) return; | |
| const canvas = canvasRef.current; | |
| const dpr = Math.max(1, 0.5 * window.devicePixelRatio); | |
| rendererRef.current = new WebGLRenderer(canvas, dpr); | |
| pointersRef.current = new PointerHandler(canvas, dpr); | |
| rendererRef.current.setup(); | |
| rendererRef.current.init(); | |
| resize(); | |
| if (rendererRef.current.test(defaultShaderSource) === null) { | |
| rendererRef.current.updateShader(defaultShaderSource); | |
| } | |
| loop(0); | |
| window.addEventListener('resize', resize); | |
| return () => { | |
| window.removeEventListener('resize', resize); | |
| if (animationFrameRef.current) { | |
| cancelAnimationFrame(animationFrameRef.current); | |
| } | |
| if (rendererRef.current) { | |
| rendererRef.current.reset(); | |
| } | |
| }; | |
| }, []); | |
| return canvasRef; | |
| }; | |
| // Reusable Hero Component | |
| const Hero: React.FC<HeroProps> = ({ | |
| trustBadge, | |
| headline, | |
| subtitle, | |
| buttons, | |
| className = "" | |
| }) => { | |
| const canvasRef = useShaderBackground(); | |
| return ( | |
| <div className={`relative w-full h-screen overflow-hidden bg-black ${className}`}> | |
| <style jsx>{` | |
| @keyframes fade-in-down { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fade-in-up { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .animate-fade-in-down { | |
| animation: fade-in-down 0.8s ease-out forwards; | |
| } | |
| .animate-fade-in-up { | |
| animation: fade-in-up 0.8s ease-out forwards; | |
| opacity: 0; | |
| } | |
| .animation-delay-200 { | |
| animation-delay: 0.2s; | |
| } | |
| .animation-delay-400 { | |
| animation-delay: 0.4s; | |
| } | |
| .animation-delay-600 { | |
| animation-delay: 0.6s; | |
| } | |
| .animation-delay-800 { | |
| animation-delay: 0.8s; | |
| } | |
| @keyframes gradient-shift { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| .animate-gradient { | |
| background-size: 200% 200%; | |
| animation: gradient-shift 3s ease infinite; | |
| } | |
| `}</style> | |
| <canvas | |
| ref={canvasRef} | |
| className="absolute inset-0 w-full h-full object-contain touch-none" | |
| style={{ background: 'black' }} | |
| /> | |
| {/* Hero Content Overlay */} | |
| <div className="absolute inset-0 z-10 flex flex-col items-center justify-center text-white"> | |
| {/* Trust Badge */} | |
| {trustBadge && ( | |
| <div className="mb-8 animate-fade-in-down"> | |
| <div className="flex items-center gap-2 px-6 py-3 bg-orange-500/10 backdrop-blur-md border border-orange-300/30 rounded-full text-sm"> | |
| {trustBadge.icons && ( | |
| <div className="flex"> | |
| {trustBadge.icons.map((icon, index) => ( | |
| <span key={index} className={`text-${index === 0 ? 'yellow' : index === 1 ? 'orange' : 'amber'}-300`}> | |
| {icon} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| <span className="text-orange-100">{trustBadge.text}</span> | |
| </div> | |
| </div> | |
| )} | |
| <div className="text-center space-y-6 max-w-5xl mx-auto px-4"> | |
| {/* Main Heading with Animation */} | |
| <div className="space-y-2"> | |
| <h1 className="text-5xl md:text-7xl lg:text-8xl font-bold bg-linear-to-r from-orange-300 via-yellow-400 to-amber-300 bg-clip-text text-transparent animate-fade-in-up animation-delay-200"> | |
| {headline.line1} | |
| </h1> | |
| <h1 className="text-5xl md:text-7xl lg:text-8xl font-bold bg-linear-to-r from-yellow-300 via-orange-400 to-red-400 bg-clip-text text-transparent animate-fade-in-up animation-delay-400"> | |
| {headline.line2} | |
| </h1> | |
| </div> | |
| {/* Subtitle with Animation */} | |
| <div className="max-w-3xl mx-auto animate-fade-in-up animation-delay-600"> | |
| <p className="text-lg md:text-xl lg:text-2xl text-orange-100/90 font-light leading-relaxed"> | |
| {subtitle} | |
| </p> | |
| </div> | |
| {/* CTA Buttons with Animation */} | |
| {buttons && ( | |
| <div className="flex flex-col sm:flex-row gap-4 justify-center mt-10 animate-fade-in-up animation-delay-800"> | |
| {buttons.primary && ( | |
| <Button variant="primary" onClick={buttons.primary.onClick}> | |
| {buttons.primary.text} | |
| </Button> | |
| )} | |
| {buttons.secondary && ( | |
| <Button variant="secondary" onClick={buttons.secondary.onClick}> | |
| {buttons.secondary.text} | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const defaultShaderSource = `#version 300 es | |
| /********* | |
| * made by Matthias Hurrle (@atzedent) | |
| * | |
| To explore strange new worlds, to seek out new life | |
| *and new civilizations, to boldly go where no man has | |
| *gone before. | |
| */ | |
| precision highp float; | |
| out vec4 O; | |
| uniform vec2 resolution; | |
| uniform float time; | |
| #define FC gl_FragCoord.xy | |
| #define T time | |
| #define R resolution | |
| #define MN min(R.x,R.y) | |
| // Returns a pseudo random number for a given point (white noise) | |
| float rnd(vec2 p) { | |
| p=fract(p*vec2(12.9898,78.233)); | |
| p+=dot(p,p+34.56); | |
| return fract(p.x*p.y); | |
| } | |
| // Returns a pseudo random number for a given point (value noise) | |
| float noise(in vec2 p) { | |
| vec2 i=floor(p), f=fract(p), u=f*f*(3.-2.*f); | |
| float | |
| a=rnd(i), | |
| b=rnd(i+vec2(1,0)), | |
| c=rnd(i+vec2(0,1)), | |
| d=rnd(i+1.); | |
| return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); | |
| } | |
| // Returns a pseudo random number for a given point (fractal noise) | |
| float fbm(vec2 p) { | |
| float t=.0, a=1.; mat2 m=mat2(1.,-.5,.2,1.2); | |
| for (int i=0; i<5; i++) { | |
| t+=a*noise(p); | |
| p*=2.*m; | |
| a*=.5; | |
| } | |
| return t; | |
| } | |
| float clouds(vec2 p) { | |
| float d=1., t=.0; | |
| for (float i=.0; i<3.; i++) { | |
| float a=d*fbm(i*10.+p.x*.2+.2*(1.+i)*p.y+d+i*i+p); | |
| t=mix(t,d,a); | |
| d=a; | |
| p*=2./(i+1.); | |
| } | |
| return t; | |
| } | |
| void main(void) { | |
| vec2 uv=(FC-.5*R)/MN,st=uv*vec2(2,1); | |
| vec3 col=vec3(0); | |
| float bg=clouds(vec2(st.x+T*.5,-st.y)); | |
| uv*=1.-.3*(sin(T*.2)*.5+.5); | |
| for (float i=1.; i<12.; i++) { | |
| uv+=.1*cos(i*vec2(.1+.01*i, .8)+i*i+T*.5+.1*uv.x); | |
| vec2 p=uv; | |
| float d=length(p); | |
| col+=.00125/d*(cos(sin(i)*vec3(1,2,3))+1.); | |
| float b=noise(i+p+bg*1.731); | |
| col+=.002*b/length(max(p,vec2(b*p.x*.02,p.y))); | |
| col=mix(col,vec3(bg*.25,bg*.137,bg*.05),d); | |
| } | |
| O=vec4(col,1); | |
| }`; | |
| export default Hero; | |