diff options
Diffstat (limited to 'src/components/ui/canvas-reveal-effect.tsx')
-rw-r--r-- | src/components/ui/canvas-reveal-effect.tsx | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/src/components/ui/canvas-reveal-effect.tsx b/src/components/ui/canvas-reveal-effect.tsx new file mode 100644 index 0000000..f84d7a8 --- /dev/null +++ b/src/components/ui/canvas-reveal-effect.tsx @@ -0,0 +1,308 @@ +"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 = false, +}: { + /** + * 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; + in vec2 fragCoord; + + 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; + out vec4 fragColor; + 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 = fragCoord.xy; + ${ + 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} + + fragColor = vec4(color, opacity); + fragColor.rgb *= 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, type: "1f" }; + break; + case "uniform3f": + preparedUniforms[uniformName] = { + value: new THREE.Vector3().fromArray(uniform.value), + type: "3f", + }; + break; + case "uniform1fv": + preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }; + break; + case "uniform3fv": + preparedUniforms[uniformName] = { + value: uniform.value.map((v: number[]) => + new THREE.Vector3().fromArray(v) + ), + type: "3fv", + }; + break; + case "uniform2f": + preparedUniforms[uniformName] = { + value: new THREE.Vector2().fromArray(uniform.value), + type: "2f", + }; + break; + default: + console.error(`Invalid uniform type for '${uniformName}'.`); + break; + } + } + + preparedUniforms["u_time"] = { value: 0, type: "1f" }; + preparedUniforms["u_resolution"] = { + value: new THREE.Vector2(size.width * 2, size.height * 2), + }; // Initialize u_resolution + return preparedUniforms; + }; + + // Shader material + const material = useMemo(() => { + const materialObject = new THREE.ShaderMaterial({ + vertexShader: ` + precision mediump float; + in vec2 coordinates; + uniform vec2 u_resolution; + out vec2 fragCoord; + void main(){ + float x = position.x; + float y = position.y; + gl_Position = vec4(x, y, 0.0, 1.0); + fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution; + fragCoord.y = u_resolution.y - fragCoord.y; + } + `, + fragmentShader: source, + uniforms: getUniforms(), + glslVersion: THREE.GLSL3, + 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; +} |