diff --git a/components/Approach.tsx b/components/Approach.tsx new file mode 100644 index 0000000..4ae51a6 --- /dev/null +++ b/components/Approach.tsx @@ -0,0 +1,141 @@ +"use client"; +import React from "react"; + +import { AnimatePresence, motion } from "framer-motion"; +import { CanvasRevealEffect } from "./ui/CanvasRevealEffect"; + +const Approach = () => { + return ( +
+

+ Project Approach +

+
+ } + description="I initiate projects by gathering user requirements, conducting research and designing the applications architecture and user experience. Using Github Projects I organise tasks into boards, plan sprints and track progress, ensuring a streamlined approach to development" + > + + + } + description="I bring projects to life by crafting responsive front-end interfaces and handling back-end logic and databases. I manage code reviews, collaborate with team members and ensure smooth integration of features" + > + + {/* Radial gradient for the cute fade */} +
+ + } + > + + +
+
+ ); +}; + +const Card = ({ + title, + icon, + children, + description, +}: { + title: string; + icon: React.ReactNode; + children?: React.ReactNode; + description: string; +}) => { + const [hovered, setHovered] = React.useState(false); + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className="border border-black/[0.2] group/canvas-card flex items-center justify-center dark:border-white/[0.2] max-w-sm w-full mx-auto p-4 relative lg:h-[35rem] rounded-3xl" + > + + + + + + + {hovered && ( + + {children} + + )} + + +
+
+ {icon} +
+

+ {title} +

+

+ {description} +

+
+
+ ); +}; + +const AceternityIcon = ({ order }: { order: string }) => { + return ( +
+ +
+ ); +}; + +export const Icon = ({ className, ...rest }: any) => { + return ( + + + + ); +}; + +export default Approach; diff --git a/components/ui/CanvasRevealEffect.tsx b/components/ui/CanvasRevealEffect.tsx new file mode 100644 index 0000000..8cd8be3 --- /dev/null +++ b/components/ui/CanvasRevealEffect.tsx @@ -0,0 +1,308 @@ +"use client"; +import { cn } from "../../utils/cn"; +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 ( +
+
+ +
+ {showGradient && ( +
+ )} +
+ ); +}; + +interface DotMatrixProps { + colors?: number[][]; + opacities?: number[]; + totalSize?: number; + dotSize?: number; + shader?: string; + center?: ("x" | "y")[]; +} + +const DotMatrix: React.FC = ({ + 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 ( + + ); +}; + +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(); + 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 ( + + + + + ); +}; + +const Shader: React.FC = ({ source, uniforms, maxFps = 60 }) => { + return ( + + + + ); +}; +interface ShaderProps { + source: string; + uniforms: { + [key: string]: { + value: number[] | number[][] | number; + type: string; + }; + }; + maxFps?: number; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 8919139..df0e968 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,6 +7,7 @@ import RecentProjects from "../../components/RecentProjects"; import { navItems } from "../../data"; import Clients from "../../components/Clients"; import Experience from "../../components/Experience"; +import Approach from "../../components/Approach"; export default function Home() { return ( @@ -18,6 +19,7 @@ export default function Home() { +
);