Skip to content

Commit 47fc13f

Browse files
committed
feat(): Adding card background
1 parent 7fa57f5 commit 47fc13f

8 files changed

Lines changed: 439 additions & 134 deletions

File tree

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/* eslint-disable react/no-unknown-property */
2+
import { Canvas, useFrame, useThree } from '@react-three/fiber';
3+
import React, { useMemo, useRef } from 'react';
4+
import * as THREE from 'three';
5+
6+
export const CanvasRevealEffect = ({
7+
animationSpeed = 0.4,
8+
opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1],
9+
colors = [[0, 255, 255]],
10+
containerClassName,
11+
dotSize,
12+
showGradient = true,
13+
}: {
14+
/**
15+
* 0.1 - slower
16+
* 1.0 - faster
17+
*/
18+
animationSpeed?: number;
19+
opacities?: number[];
20+
colors?: number[][];
21+
containerClassName?: string;
22+
dotSize?: number;
23+
showGradient?: boolean;
24+
}) => {
25+
return (
26+
<div className={'z-0 h-full relative w-full' + containerClassName}>
27+
<div className="h-full w-full">
28+
<DotMatrix
29+
colors={colors ?? [[0, 255, 255]]}
30+
dotSize={dotSize ?? 3}
31+
opacities={
32+
opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1]
33+
}
34+
shader={`
35+
float animation_speed_factor = ${animationSpeed.toFixed(1)};
36+
float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
37+
opacity *= step(intro_offset, u_time * animation_speed_factor);
38+
opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
39+
`}
40+
center={['x', 'y']}
41+
/>
42+
</div>
43+
{showGradient && (
44+
<div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" />
45+
)}
46+
</div>
47+
);
48+
};
49+
50+
interface DotMatrixProps {
51+
colors?: number[][];
52+
opacities?: number[];
53+
totalSize?: number;
54+
dotSize?: number;
55+
shader?: string;
56+
center?: ('x' | 'y')[];
57+
}
58+
59+
const DotMatrix: React.FC<DotMatrixProps> = ({
60+
colors = [[0, 0, 0]],
61+
opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
62+
totalSize = 4,
63+
dotSize = 2,
64+
shader = '',
65+
center = ['x', 'y'],
66+
}) => {
67+
const uniforms = React.useMemo(() => {
68+
let colorsArray = [
69+
colors[0],
70+
colors[0],
71+
colors[0],
72+
colors[0],
73+
colors[0],
74+
colors[0],
75+
];
76+
if (colors.length === 2) {
77+
colorsArray = [
78+
colors[0],
79+
colors[0],
80+
colors[0],
81+
colors[1],
82+
colors[1],
83+
colors[1],
84+
];
85+
} else if (colors.length === 3) {
86+
colorsArray = [
87+
colors[0],
88+
colors[0],
89+
colors[1],
90+
colors[1],
91+
colors[2],
92+
colors[2],
93+
];
94+
}
95+
96+
return {
97+
u_colors: {
98+
value: colorsArray.map((color) => [
99+
color[0] / 255,
100+
color[1] / 255,
101+
color[2] / 255,
102+
]),
103+
type: 'uniform3fv',
104+
},
105+
u_opacities: {
106+
value: opacities,
107+
type: 'uniform1fv',
108+
},
109+
u_total_size: {
110+
value: totalSize,
111+
type: 'uniform1f',
112+
},
113+
u_dot_size: {
114+
value: dotSize,
115+
type: 'uniform1f',
116+
},
117+
};
118+
}, [colors, opacities, totalSize, dotSize]);
119+
120+
return (
121+
<Shader
122+
source={`
123+
precision mediump float;
124+
in vec2 fragCoord;
125+
126+
uniform float u_time;
127+
uniform float u_opacities[10];
128+
uniform vec3 u_colors[6];
129+
uniform float u_total_size;
130+
uniform float u_dot_size;
131+
uniform vec2 u_resolution;
132+
out vec4 fragColor;
133+
float PHI = 1.61803398874989484820459;
134+
float random(vec2 xy) {
135+
return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
136+
}
137+
float map(float value, float min1, float max1, float min2, float max2) {
138+
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
139+
}
140+
void main() {
141+
vec2 st = fragCoord.xy;
142+
${
143+
center.includes('x')
144+
? 'st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));'
145+
: ''
146+
}
147+
${
148+
center.includes('y')
149+
? 'st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));'
150+
: ''
151+
}
152+
float opacity = step(0.0, st.x);
153+
opacity *= step(0.0, st.y);
154+
155+
vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
156+
157+
float frequency = 5.0;
158+
float show_offset = random(st2);
159+
float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);
160+
opacity *= u_opacities[int(rand * 10.0)];
161+
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
162+
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
163+
164+
vec3 color = u_colors[int(show_offset * 6.0)];
165+
166+
${shader}
167+
168+
fragColor = vec4(color, opacity);
169+
fragColor.rgb *= fragColor.a;
170+
}`}
171+
uniforms={uniforms}
172+
maxFps={60}
173+
/>
174+
);
175+
};
176+
177+
type Uniforms = {
178+
[key: string]: {
179+
value: number[] | number[][] | number;
180+
type: string;
181+
};
182+
};
183+
const ShaderMaterial = ({
184+
source,
185+
uniforms,
186+
maxFps = 60,
187+
}: {
188+
source: string;
189+
hovered?: boolean;
190+
maxFps?: number;
191+
uniforms: Uniforms;
192+
}) => {
193+
const { size } = useThree();
194+
const ref = useRef<THREE.Mesh>();
195+
let lastFrameTime = 0;
196+
197+
useFrame(({ clock }) => {
198+
if (!ref.current) return;
199+
const timestamp = clock.getElapsedTime();
200+
if (timestamp - lastFrameTime < 1 / maxFps) {
201+
return;
202+
}
203+
lastFrameTime = timestamp;
204+
205+
const material: any = ref.current.material;
206+
const timeLocation = material.uniforms.u_time;
207+
timeLocation.value = timestamp;
208+
});
209+
210+
const getUniforms = () => {
211+
const preparedUniforms: any = {};
212+
213+
for (const uniformName in uniforms) {
214+
const uniform: any = uniforms[uniformName];
215+
216+
switch (uniform.type) {
217+
case 'uniform1f':
218+
preparedUniforms[uniformName] = { value: uniform.value, type: '1f' };
219+
break;
220+
case 'uniform3f':
221+
preparedUniforms[uniformName] = {
222+
value: new THREE.Vector3().fromArray(uniform.value),
223+
type: '3f',
224+
};
225+
break;
226+
case 'uniform1fv':
227+
preparedUniforms[uniformName] = { value: uniform.value, type: '1fv' };
228+
break;
229+
case 'uniform3fv':
230+
preparedUniforms[uniformName] = {
231+
value: uniform.value.map((v: number[]) =>
232+
new THREE.Vector3().fromArray(v),
233+
),
234+
type: '3fv',
235+
};
236+
break;
237+
case 'uniform2f':
238+
preparedUniforms[uniformName] = {
239+
value: new THREE.Vector2().fromArray(uniform.value),
240+
type: '2f',
241+
};
242+
break;
243+
default:
244+
console.error(`Invalid uniform type for '${uniformName}'.`);
245+
break;
246+
}
247+
}
248+
249+
preparedUniforms['u_time'] = { value: 0, type: '1f' };
250+
preparedUniforms['u_resolution'] = {
251+
value: new THREE.Vector2(size.width * 2, size.height * 2),
252+
}; // Initialize u_resolution
253+
return preparedUniforms;
254+
};
255+
256+
// Shader material
257+
const material = useMemo(() => {
258+
const materialObject = new THREE.ShaderMaterial({
259+
vertexShader: `
260+
precision mediump float;
261+
in vec2 coordinates;
262+
uniform vec2 u_resolution;
263+
out vec2 fragCoord;
264+
void main(){
265+
float x = position.x;
266+
float y = position.y;
267+
gl_Position = vec4(x, y, 0.0, 1.0);
268+
fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
269+
fragCoord.y = u_resolution.y - fragCoord.y;
270+
}
271+
`,
272+
fragmentShader: source,
273+
uniforms: getUniforms(),
274+
glslVersion: THREE.GLSL3,
275+
blending: THREE.CustomBlending,
276+
blendSrc: THREE.SrcAlphaFactor,
277+
blendDst: THREE.OneFactor,
278+
});
279+
280+
return materialObject;
281+
// eslint-disable-next-line react-hooks/exhaustive-deps
282+
}, [size.width, size.height, source]);
283+
284+
return (
285+
<mesh ref={ref as any}>
286+
<planeGeometry args={[2, 2]} />
287+
<primitive object={material} attach="material" />
288+
</mesh>
289+
);
290+
};
291+
292+
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
293+
return (
294+
<Canvas className="absolute inset-0 h-full w-full">
295+
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
296+
</Canvas>
297+
);
298+
};
299+
interface ShaderProps {
300+
source: string;
301+
uniforms: {
302+
[key: string]: {
303+
value: number[] | number[][] | number;
304+
type: string;
305+
};
306+
};
307+
maxFps?: number;
308+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use client';
2+
3+
import { useMotionValue, motion, useMotionTemplate } from 'framer-motion';
4+
import React, { MouseEvent as ReactMouseEvent, useState } from 'react';
5+
import { CanvasRevealEffect } from './RevealEffect';
6+
7+
const CardSpotlight = ({
8+
children,
9+
radius = 350,
10+
color = 'transparent',
11+
...props
12+
}: {
13+
radius?: number;
14+
color?: string;
15+
children: React.ReactNode;
16+
} & React.HTMLAttributes<HTMLDivElement>) => {
17+
const mouseX = useMotionValue(0);
18+
const mouseY = useMotionValue(0);
19+
function handleMouseMove({
20+
currentTarget,
21+
clientX,
22+
clientY,
23+
}: ReactMouseEvent<HTMLDivElement>) {
24+
const { left, top } = currentTarget.getBoundingClientRect();
25+
26+
mouseX.set(clientX - left);
27+
mouseY.set(clientY - top);
28+
}
29+
30+
const [isHovering, setIsHovering] = useState(false);
31+
const handleMouseEnter = () => setIsHovering(true);
32+
const handleMouseLeave = () => setIsHovering(false);
33+
return (
34+
<div
35+
className={
36+
'group/spotlight p-10 rounded-md relative border border-neutral-800 bg-black dark:border-neutral-800 '
37+
}
38+
onMouseMove={handleMouseMove}
39+
onMouseEnter={handleMouseEnter}
40+
onMouseLeave={handleMouseLeave}
41+
{...props}
42+
>
43+
<motion.div
44+
className="pointer-events-none absolute z-0 -inset-px rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
45+
style={{
46+
backgroundColor: color,
47+
maskImage: useMotionTemplate`
48+
radial-gradient(
49+
${radius}px circle at ${mouseX}px ${mouseY}px,
50+
white,
51+
transparent 80%
52+
)
53+
`,
54+
}}
55+
>
56+
{isHovering && (
57+
<CanvasRevealEffect
58+
animationSpeed={5}
59+
containerClassName="bg-transparent absolute inset-0 pointer-events-none"
60+
colors={[
61+
[59, 130, 246],
62+
[139, 92, 246],
63+
]}
64+
dotSize={3}
65+
/>
66+
)}
67+
</motion.div>
68+
{children}
69+
</div>
70+
);
71+
};
72+
73+
export default CardSpotlight;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as CardSpotlight } from './Spotlight';

0 commit comments

Comments
 (0)