project card effect
This commit is contained in:
@ -1,26 +1,80 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import Link from "next/link"
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import ProjectCardDropdown from "./dropdown";
|
||||
import { Clock, Globe, Lock } from "lucide-react";
|
||||
import { Sandbox } from "@/lib/types";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
export default function ProjectCard({
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
sandbox,
|
||||
onVisibilityChange,
|
||||
onDelete,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
id: string
|
||||
className?: string
|
||||
children?: React.ReactNode;
|
||||
sandbox: Sandbox;
|
||||
onVisibilityChange: (sandbox: Sandbox) => void;
|
||||
onDelete: (sandbox: Sandbox) => void;
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/code/${id}`}
|
||||
className={cn(
|
||||
className,
|
||||
"rounded-lg border bg-card text-card-foreground shadow h-48 p-[1px] gradient-project-card-bg cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring"
|
||||
)}
|
||||
<Card
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className="group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-foreground transition-all relative overflow-hidden"
|
||||
>
|
||||
<div className="rounded-[7px] p-4 h-full flex flex-col justify-between gradient-project-card">
|
||||
{children}
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||
<Image
|
||||
alt=""
|
||||
src={
|
||||
sandbox.type === "react"
|
||||
? "/project-icons/react.svg"
|
||||
: "/project-icons/node.svg"
|
||||
}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
|
||||
{sandbox.name}
|
||||
</div>
|
||||
<ProjectCardDropdown
|
||||
sandbox={sandbox}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm z-10">
|
||||
<div className="flex items-center">
|
||||
{sandbox.visibility === "private" ? (
|
||||
<>
|
||||
<Lock className="w-3 h-3 mr-2" /> Private
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Globe className="w-3 h-3 mr-2" /> Public
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-3 h-3 mr-2" /> 3d ago
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
304
frontend/components/dashboard/projectCard/revealEffect.tsx
Normal file
304
frontend/components/dashboard/projectCard/revealEffect.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
"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,
|
||||
}: {
|
||||
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-neutral-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;
|
||||
}
|
Reference in New Issue
Block a user