Spaces:
Running
Running
"use client"; | |
import { cn } from "@/lib/utils"; | |
import { Canvas, useFrame, useThree } from "@react-three/fiber"; | |
import React, { useMemo, useRef } from "react"; | |
import * as THREE from "three"; | |
export const CanvasRevealEffect = ({ | |
animationSpeed = 0.4, | |
opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1], | |
colors = [[0, 255, 255]], | |
containerClassName, | |
dotSize, | |
showGradient = true, | |
}: { | |
/** | |
* 0.1 - slower | |
* 1.0 - faster | |
*/ | |
animationSpeed?: number; | |
opacities?: number[]; | |
colors?: number[][]; | |
containerClassName?: string; | |
dotSize?: number; | |
showGradient?: boolean; | |
}) => { | |
return ( | |
<div className={cn("h-full relative bg-white w-full", containerClassName)}> | |
<div className="h-full w-full"> | |
<DotMatrix | |
colors={colors ?? [[0, 255, 255]]} | |
dotSize={dotSize ?? 3} | |
opacities={ | |
opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1] | |
} | |
shader={` | |
float animation_speed_factor = ${animationSpeed.toFixed(1)}; | |
float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15); | |
opacity *= step(intro_offset, u_time * animation_speed_factor); | |
opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25); | |
`} | |
center={["x", "y"]} | |
/> | |
</div> | |
{showGradient && ( | |
<div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" /> | |
)} | |
</div> | |
); | |
}; | |
interface DotMatrixProps { | |
colors?: number[][]; | |
opacities?: number[]; | |
totalSize?: number; | |
dotSize?: number; | |
shader?: string; | |
center?: ("x" | "y")[]; | |
} | |
const DotMatrix: React.FC<DotMatrixProps> = ({ | |
colors = [[0, 0, 0]], | |
opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14], | |
totalSize = 4, | |
dotSize = 2, | |
shader = "", | |
center = ["x", "y"], | |
}) => { | |
const uniforms = React.useMemo(() => { | |
let colorsArray = [ | |
colors[0], | |
colors[0], | |
colors[0], | |
colors[0], | |
colors[0], | |
colors[0], | |
]; | |
if (colors.length === 2) { | |
colorsArray = [ | |
colors[0], | |
colors[0], | |
colors[0], | |
colors[1], | |
colors[1], | |
colors[1], | |
]; | |
} else if (colors.length === 3) { | |
colorsArray = [ | |
colors[0], | |
colors[0], | |
colors[1], | |
colors[1], | |
colors[2], | |
colors[2], | |
]; | |
} | |
return { | |
u_colors: { | |
value: colorsArray.map((color) => [ | |
color[0] / 255, | |
color[1] / 255, | |
color[2] / 255, | |
]), | |
type: "uniform3fv", | |
}, | |
u_opacities: { | |
value: opacities, | |
type: "uniform1fv", | |
}, | |
u_total_size: { | |
value: totalSize, | |
type: "uniform1f", | |
}, | |
u_dot_size: { | |
value: dotSize, | |
type: "uniform1f", | |
}, | |
}; | |
}, [colors, opacities, totalSize, dotSize]); | |
return ( | |
<Shader | |
source={` | |
precision mediump float; | |
varying vec2 vUv; | |
uniform float u_time; | |
uniform float u_opacities[10]; | |
uniform vec3 u_colors[6]; | |
uniform float u_total_size; | |
uniform float u_dot_size; | |
uniform vec2 u_resolution; | |
float PHI = 1.61803398874989484820459; | |
float random(vec2 xy) { | |
return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x); | |
} | |
float map(float value, float min1, float max1, float min2, float max2) { | |
return min2 + (value - min1) * (max2 - min2) / (max1 - min1); | |
} | |
void main() { | |
vec2 st = vUv * u_resolution; | |
${ | |
center.includes("x") | |
? "st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));" | |
: "" | |
} | |
${ | |
center.includes("y") | |
? "st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));" | |
: "" | |
} | |
float opacity = step(0.0, st.x); | |
opacity *= step(0.0, st.y); | |
vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size)); | |
float frequency = 5.0; | |
float show_offset = random(st2); | |
float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0); | |
opacity *= u_opacities[int(rand * 10.0)]; | |
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size)); | |
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size)); | |
vec3 color = u_colors[int(show_offset * 6.0)]; | |
${shader} | |
gl_FragColor = vec4(color, opacity); | |
gl_FragColor.rgb *= gl_FragColor.a; | |
}`} | |
uniforms={uniforms} | |
maxFps={60} | |
/> | |
); | |
}; | |
type Uniforms = { | |
[key: string]: { | |
value: number[] | number[][] | number; | |
type: string; | |
}; | |
}; | |
const ShaderMaterial = ({ | |
source, | |
uniforms, | |
maxFps = 60, | |
}: { | |
source: string; | |
hovered?: boolean; | |
maxFps?: number; | |
uniforms: Uniforms; | |
}) => { | |
const { size } = useThree(); | |
const ref = useRef<THREE.Mesh>(); | |
let lastFrameTime = 0; | |
useFrame(({ clock }) => { | |
if (!ref.current) return; | |
const timestamp = clock.getElapsedTime(); | |
if (timestamp - lastFrameTime < 1 / maxFps) { | |
return; | |
} | |
lastFrameTime = timestamp; | |
const material: any = ref.current.material; | |
const timeLocation = material.uniforms.u_time; | |
timeLocation.value = timestamp; | |
}); | |
const getUniforms = () => { | |
const preparedUniforms: any = {}; | |
for (const uniformName in uniforms) { | |
const uniform: any = uniforms[uniformName]; | |
switch (uniform.type) { | |
case "uniform1f": | |
preparedUniforms[uniformName] = { value: uniform.value }; | |
break; | |
case "uniform3f": | |
preparedUniforms[uniformName] = { | |
value: new THREE.Vector3().fromArray(uniform.value), | |
}; | |
break; | |
case "uniform1fv": | |
preparedUniforms[uniformName] = { value: uniform.value }; | |
break; | |
case "uniform3fv": | |
preparedUniforms[uniformName] = { | |
value: uniform.value.map((v: number[]) => | |
new THREE.Vector3().fromArray(v) | |
), | |
}; | |
break; | |
case "uniform2f": | |
preparedUniforms[uniformName] = { | |
value: new THREE.Vector2().fromArray(uniform.value), | |
}; | |
break; | |
default: | |
console.error(`Invalid uniform type for '${uniformName}'.`); | |
break; | |
} | |
} | |
preparedUniforms["u_time"] = { value: 0 }; | |
preparedUniforms["u_resolution"] = { | |
value: new THREE.Vector2(size.width * 2, size.height * 2), | |
}; | |
return preparedUniforms; | |
}; | |
// Shader material | |
const material = useMemo(() => { | |
const materialObject = new THREE.ShaderMaterial({ | |
vertexShader: ` | |
precision mediump float; | |
attribute vec2 uv; | |
attribute vec3 position; | |
uniform mat4 modelViewMatrix; | |
uniform mat4 projectionMatrix; | |
varying vec2 vUv; | |
void main(){ | |
vUv = uv; | |
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
} | |
`, | |
fragmentShader: source, | |
uniforms: getUniforms(), | |
transparent: true, | |
blending: THREE.CustomBlending, | |
blendSrc: THREE.SrcAlphaFactor, | |
blendDst: THREE.OneFactor, | |
}); | |
return materialObject; | |
}, [size.width, size.height, source]); | |
return ( | |
<mesh ref={ref as any}> | |
<planeGeometry args={[2, 2]} /> | |
<primitive object={material} attach="material" /> | |
</mesh> | |
); | |
}; | |
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => { | |
return ( | |
<Canvas className="absolute inset-0 h-full w-full"> | |
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} /> | |
</Canvas> | |
); | |
}; | |
interface ShaderProps { | |
source: string; | |
uniforms: { | |
[key: string]: { | |
value: number[] | number[][] | number; | |
type: string; | |
}; | |
}; | |
maxFps?: number; | |
} |